miniflux-legacy/model.php

885 lines
21 KiB
PHP
Raw Normal View History

2013-02-18 03:48:21 +01:00
<?php
namespace Model;
require_once 'vendor/PicoFeed/Encoding.php';
require_once 'vendor/PicoFeed/Filter.php';
require_once 'vendor/PicoFeed/Client.php';
2013-02-18 03:48:21 +01:00
require_once 'vendor/PicoFeed/Export.php';
require_once 'vendor/PicoFeed/Import.php';
require_once 'vendor/PicoFeed/Reader.php';
require_once 'vendor/SimpleValidator/Validator.php';
require_once 'vendor/SimpleValidator/Base.php';
require_once 'vendor/SimpleValidator/Validators/Required.php';
require_once 'vendor/SimpleValidator/Validators/Unique.php';
require_once 'vendor/SimpleValidator/Validators/MaxLength.php';
require_once 'vendor/SimpleValidator/Validators/MinLength.php';
require_once 'vendor/SimpleValidator/Validators/Integer.php';
require_once 'vendor/SimpleValidator/Validators/Equals.php';
require_once 'vendor/SimpleValidator/Validators/Integer.php';
2013-02-18 03:48:21 +01:00
use SimpleValidator\Validator;
use SimpleValidator\Validators;
use PicoFeed\Import;
use PicoFeed\Reader;
use PicoFeed\Export;
2013-08-10 04:23:57 +02:00
const DB_VERSION = 14;
const HTTP_USERAGENT = 'Miniflux - http://miniflux.net';
const LIMIT_ALL = -1;
2013-04-13 03:08:55 +02:00
function get_languages()
{
return array(
2013-07-13 10:36:21 +02:00
'cs_CZ' => t('Czech'),
2013-04-13 03:08:55 +02:00
'en_US' => t('English'),
2013-07-02 00:02:47 +02:00
'fr_FR' => t('French'),
2013-07-08 20:56:26 +02:00
'de_DE' => t('German'),
'it_IT' => t('Italian'),
'zh_CN' => t('Simplified Chinese'),
2013-04-13 03:08:55 +02:00
);
}
2013-07-17 03:58:11 +02:00
function get_themes()
{
$themes = array(
'original' => t('Original')
);
if (file_exists(THEME_DIRECTORY)) {
$dir = new \DirectoryIterator(THEME_DIRECTORY);
foreach ($dir as $fileinfo) {
if (! $fileinfo->isDot() && $fileinfo->isDir()) {
$themes[$dir->getFilename()] = ucfirst($dir->getFilename());
}
}
}
return $themes;
}
2013-05-26 19:09:34 +02:00
function get_autoflush_options()
{
return array(
'0' => t('Never'),
'1' => t('After %d day', 1),
'5' => t('After %d days', 5),
'15' => t('After %d days', 15),
'30' => t('After %d days', 30)
);
}
function get_paging_options()
{
return array(
50 => 50,
100 => 100,
150 => 150,
200 => 200,
250 => 250,
);
}
function write_debug()
{
if (DEBUG) {
file_put_contents(
DEBUG_DIRECTORY.'/miniflux_'.date('YmdH').'.debug',
var_export(\PicoFeed\Logging::$messages, true).PHP_EOL,
FILE_APPEND | LOCK_EX
);
}
}
function generate_token()
2013-07-28 21:44:51 +02:00
{
if (ini_get('open_basedir') === '') {
return substr(base64_encode(file_get_contents('/dev/urandom', false, null, 0, 20)), 0, 15);
}
else {
return substr(base64_encode(uniqid(mt_rand(), true)), 0, 20);
}
}
function new_tokens()
{
$values = array(
'api_token' => generate_token(),
'feed_token' => generate_token(),
);
return \PicoTools\singleton('db')->table('config')->update($values);
2013-07-28 21:44:51 +02:00
}
2013-02-18 03:48:21 +01:00
function export_feeds()
{
$opml = new Export(get_feeds());
return $opml->execute();
}
function import_feeds($content)
{
$import = new Import($content);
$feeds = $import->execute();
if ($feeds) {
$db = \PicoTools\singleton('db');
$db->startTransaction();
foreach ($feeds as $feed) {
if (! $db->table('feeds')->eq('feed_url', $feed->feed_url)->count()) {
$db->table('feeds')->save(array(
'title' => $feed->title,
'site_url' => $feed->site_url,
'feed_url' => $feed->feed_url
));
}
}
$db->closeTransaction();
return true;
}
return false;
}
function import_feed($url)
{
$reader = new Reader;
$resource = $reader->download($url, '', '', HTTP_TIMEOUT, HTTP_USERAGENT);
2013-02-18 03:48:21 +01:00
$parser = $reader->getParser();
if ($parser !== false) {
$feed = $parser->execute();
if ($feed === false) return false;
if (! $feed->title || ! $feed->url) return false;
2013-04-04 03:19:02 +02:00
2013-02-18 03:48:21 +01:00
$db = \PicoTools\singleton('db');
if (! $db->table('feeds')->eq('feed_url', $reader->getUrl())->count()) {
// Etag and LastModified are added the next update
2013-02-18 03:48:21 +01:00
$rs = $db->table('feeds')->save(array(
'title' => $feed->title,
'site_url' => $feed->url,
'feed_url' => $reader->getUrl()
));
if ($rs) {
$feed_id = $db->getConnection()->getLastId();
update_items($feed_id, $feed->items);
2013-07-28 21:44:51 +02:00
return (int) $feed_id;
2013-02-18 03:48:21 +01:00
}
}
}
return false;
}
2013-05-21 12:25:13 +02:00
2013-05-18 20:35:16 +02:00
function update_feeds($limit = LIMIT_ALL)
{
2013-05-18 20:35:16 +02:00
$feeds_id = get_feeds_id($limit);
foreach ($feeds_id as $feed_id) {
update_feed($feed_id);
}
// Auto-vacuum for people using the cronjob
\PicoTools\singleton('db')->getConnection()->exec('VACUUM');
}
function update_feed($feed_id)
{
$feed = get_feed($feed_id);
2013-07-28 21:44:51 +02:00
if (empty($feed)) return false;
$reader = new Reader;
$resource = $reader->download(
$feed['feed_url'],
$feed['last_modified'],
$feed['etag'],
HTTP_TIMEOUT,
HTTP_USERAGENT
);
2013-05-21 12:25:13 +02:00
// Update the `last_checked` column each time, HTTP cache or not
2013-05-18 20:35:16 +02:00
update_feed_last_checked($feed_id);
if (! $resource->isModified()) return true;
$parser = $reader->getParser();
if ($parser !== false) {
$feed = $parser->execute();
if ($feed !== false) {
update_feed_cache_infos($feed_id, $resource->getLastModified(), $resource->getEtag());
update_items($feed_id, $feed->items);
return true;
}
}
return false;
}
2013-05-18 20:35:16 +02:00
function get_feeds_id($limit = LIMIT_ALL)
{
2013-05-18 20:35:16 +02:00
$table_feeds = \PicoTools\singleton('db')->table('feeds')
->eq('enabled', 1)
2013-05-18 21:47:22 +02:00
->asc('last_checked');
2013-05-18 20:35:16 +02:00
2013-05-21 12:25:13 +02:00
if ($limit !== LIMIT_ALL) {
2013-05-18 20:35:16 +02:00
$table_feeds->limit((int)$limit);
}
return $table_feeds->listing('id', 'id');
}
2013-02-18 03:48:21 +01:00
function get_feeds()
{
return \PicoTools\singleton('db')
->table('feeds')
->asc('title')
->findAll();
}
function get_feed($feed_id)
{
return \PicoTools\singleton('db')
->table('feeds')
->eq('id', $feed_id)
->findOne();
}
2013-07-13 02:26:47 +02:00
function get_empty_feeds()
{
$feeds = \PicoTools\singleton('db')
->table('feeds')
2013-07-13 02:26:47 +02:00
->columns('feeds.id', 'feeds.title', 'COUNT(items.id) AS nb_items')
->join('items', 'feed_id', 'id')
->isNull('feeds.last_checked')
2013-07-17 02:43:10 +02:00
->groupBy('feeds.id')
2013-07-13 02:26:47 +02:00
->findAll();
2013-07-13 02:26:47 +02:00
foreach ($feeds as $key => &$feed) {
if ($feed['nb_items'] > 0) {
unset($feeds[$key]);
}
}
2013-07-13 02:26:47 +02:00
return $feeds;
}
2013-05-21 12:25:13 +02:00
function update_feed_last_checked($feed_id)
{
2013-05-18 20:35:16 +02:00
\PicoTools\singleton('db')
->table('feeds')
->eq('id', $feed_id)
->save(array(
2013-05-21 12:25:13 +02:00
'last_checked' => time()
2013-05-18 20:35:16 +02:00
));
}
2013-02-18 03:48:21 +01:00
2013-05-21 12:25:13 +02:00
function update_feed_cache_infos($feed_id, $last_modified, $etag)
{
\PicoTools\singleton('db')
->table('feeds')
->eq('id', $feed_id)
->save(array(
'last_modified' => $last_modified,
2013-05-21 12:25:13 +02:00
'etag' => $etag
));
}
function download_item($item_id)
{
require_once 'vendor/Readability/Readability.php';
$item = get_item($item_id);
$client = \PicoFeed\Client::create();
$client->url = $item['url'];
$client->timeout = HTTP_TIMEOUT;
$client->user_agent = HTTP_USERAGENT;
$client->execute();
$content = $client->getContent();
if (! empty($content)) {
$content = \PicoFeed\Encoding::toUTF8($content);
$readability = new \Readability($content, $item['url']);
if ($readability->init()) {
// Get relevant content
$content = $readability->getContent()->innerHTML;
// Filter content
$filter = new \PicoFeed\Filter($content, $item['url']);
$content = $filter->execute();
$nocontent = (bool) get_config_value('nocontent');
if ($nocontent === false) {
// Save content
\PicoTools\singleton('db')
->table('items')
->eq('id', $item['id'])
->save(array('content' => $content));
}
return array(
'result' => true,
'content' => $content
);
}
}
return array(
'result' => false,
'content' => ''
);
}
2013-02-18 03:48:21 +01:00
function remove_feed($feed_id)
{
// Items are removed by a sql constraint
return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->remove();
}
function enable_feed($feed_id)
{
return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 1)));
}
function disable_feed($feed_id)
{
return \PicoTools\singleton('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 0)));
2013-02-18 03:48:21 +01:00
}
2013-07-06 04:37:19 +02:00
function get_unread_items($offset = null, $limit = null)
2013-02-18 03:48:21 +01:00
{
return \PicoTools\singleton('db')
->table('items')
->columns('items.id', 'items.title', 'items.updated', 'items.url', 'items.content', 'items.bookmark', 'items.status', 'items.feed_id', 'feeds.site_url', 'feeds.title AS feed_title')
2013-02-18 03:48:21 +01:00
->join('feeds', 'id', 'feed_id')
->eq('status', 'unread')
->desc('updated')
2013-07-06 04:37:19 +02:00
->offset($offset)
->limit($limit)
2013-02-18 03:48:21 +01:00
->findAll();
}
2013-07-06 04:37:19 +02:00
function count_items($status)
{
return \PicoTools\singleton('db')
->table('items')
->eq('status', $status)
->count();
}
function get_read_items($offset = null, $limit = null)
2013-02-18 03:48:21 +01:00
{
return \PicoTools\singleton('db')
->table('items')
->columns('items.id', 'items.title', 'items.updated', 'items.url', 'items.bookmark', 'items.feed_id', 'feeds.site_url', 'feeds.title AS feed_title')
2013-02-18 03:48:21 +01:00
->join('feeds', 'id', 'feed_id')
->eq('status', 'read')
->desc('updated')
2013-07-06 04:37:19 +02:00
->offset($offset)
->limit($limit)
2013-02-18 03:48:21 +01:00
->findAll();
}
2013-07-06 04:37:19 +02:00
function count_bookmarks()
{
return \PicoTools\singleton('db')
->table('items')
->eq('bookmark', 1)
->in('status', array('read', 'unread'))
2013-07-06 04:37:19 +02:00
->count();
}
function get_bookmarks($offset = null, $limit = null)
{
return \PicoTools\singleton('db')
->table('items')
->columns('items.id', 'items.title', 'items.updated', 'items.url', 'items.status', 'items.feed_id', 'feeds.site_url', 'feeds.title AS feed_title')
->join('feeds', 'id', 'feed_id')
2013-06-15 05:12:08 +02:00
->in('status', array('read', 'unread'))
->eq('bookmark', 1)
->desc('updated')
2013-07-06 04:37:19 +02:00
->offset($offset)
->limit($limit)
->findAll();
2013-02-18 03:48:21 +01:00
}
function count_feed_items($feed_id)
{
return \PicoTools\singleton('db')
->table('items')
->eq('feed_id', $feed_id)
->in('status', array('read', 'unread'))
->count();
}
function get_feed_items($feed_id, $offset = null, $limit = null)
{
return \PicoTools\singleton('db')
->table('items')
->columns('items.id', 'items.title', 'items.updated', 'items.url', 'items.feed_id', 'items.status', 'items.bookmark', 'feeds.site_url')
->join('feeds', 'id', 'feed_id')
->in('status', array('read', 'unread'))
->eq('feed_id', $feed_id)
->desc('updated')
->offset($offset)
->limit($limit)
->findAll();
}
2013-02-18 03:48:21 +01:00
function get_item($id)
{
return \PicoTools\singleton('db')
2013-02-18 03:48:21 +01:00
->table('items')
->eq('id', $id)
->findOne();
}
2013-05-26 19:09:34 +02:00
2013-02-18 03:48:21 +01:00
function get_nav_item($item, $status = array('unread'), $bookmark = array(1, 0), $feed_id = null)
{
$query = \PicoTools\singleton('db')
->table('items')
->columns('id', 'status', 'title', 'bookmark')
2013-08-04 20:15:32 +02:00
->neq('status', 'removed')
->desc('updated');
if ($feed_id) $query->eq('feed_id', $feed_id);
$items = $query->findAll();
$next_item = null;
$previous_item = null;
2013-08-04 20:15:32 +02:00
for ($i = 0, $ilen = count($items); $i < $ilen; $i++) {
if ($items[$i]['id'] == $item['id']) {
if ($i > 0) {
$j = $i - 1;
2013-08-04 20:15:32 +02:00
while ($j >= 0) {
if (in_array($items[$j]['status'], $status) && in_array($items[$j]['bookmark'], $bookmark)) {
2013-08-04 20:15:32 +02:00
$previous_item = $items[$j];
break;
}
$j--;
}
}
if ($i < ($ilen - 1)) {
$j = $i + 1;
while ($j < $ilen) {
if (in_array($items[$j]['status'], $status) && in_array($items[$j]['bookmark'], $bookmark)) {
2013-08-04 20:15:32 +02:00
$next_item = $items[$j];
break;
}
$j++;
}
}
break;
}
}
return array(
'next' => $next_item,
'previous' => $previous_item
);
}
function set_item_removed($id)
{
2013-07-28 21:44:51 +02:00
return \PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'removed', 'content' => ''));
}
2013-02-18 03:48:21 +01:00
function set_item_read($id)
{
2013-07-28 21:44:51 +02:00
return \PicoTools\singleton('db')
2013-02-18 03:48:21 +01:00
->table('items')
->eq('id', $id)
->save(array('status' => 'read'));
}
2013-04-03 04:49:14 +02:00
function set_item_unread($id)
{
2013-07-28 21:44:51 +02:00
return \PicoTools\singleton('db')
2013-04-03 04:49:14 +02:00
->table('items')
->eq('id', $id)
->save(array('status' => 'unread'));
}
2013-06-15 05:12:08 +02:00
function set_bookmark_value($id, $value)
{
2013-07-28 21:44:51 +02:00
return \PicoTools\singleton('db')
->table('items')
->eq('id', $id)
2013-06-15 05:12:08 +02:00
->save(array('bookmark' => $value));
}
2013-04-03 04:49:14 +02:00
function switch_item_status($id)
{
$item = \PicoTools\singleton('db')
->table('items')
->columns('status')
->eq('id', $id)
->findOne();
if ($item['status'] == 'unread') {
\PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'read'));
return 'read';
}
else {
\PicoTools\singleton('db')
->table('items')
->eq('id', $id)
->save(array('status' => 'unread'));
return 'unread';
}
return '';
}
2013-07-18 00:55:28 +02:00
// Mark all items as read
2013-04-03 04:49:14 +02:00
function mark_as_read()
{
2013-07-28 21:44:51 +02:00
return \PicoTools\singleton('db')
2013-04-03 04:49:14 +02:00
->table('items')
->eq('status', 'unread')
->save(array('status' => 'read'));
}
2013-07-18 00:55:28 +02:00
// Mark only specified items as read
function mark_items_as_read(array $items_id)
{
\PicoTools\singleton('db')->startTransaction();
foreach ($items_id as $id) {
set_item_read($id);
2013-07-18 00:55:28 +02:00
}
\PicoTools\singleton('db')->closeTransaction();
}
2013-05-26 19:09:34 +02:00
function mark_as_removed()
2013-02-18 03:48:21 +01:00
{
2013-07-28 21:44:51 +02:00
return \PicoTools\singleton('db')
2013-02-18 03:48:21 +01:00
->table('items')
2013-05-26 19:09:34 +02:00
->eq('status', 'read')
->eq('bookmark', 0)
->save(array('status' => 'removed', 'content' => ''));
2013-02-18 03:48:21 +01:00
}
2013-05-26 19:09:34 +02:00
function autoflush()
{
$autoflush = get_config_value('autoflush');
2013-05-26 19:09:34 +02:00
if ($autoflush) {
2013-05-26 19:09:34 +02:00
\PicoTools\singleton('db')
->table('items')
2013-06-15 14:45:43 +02:00
->eq('bookmark', 0)
2013-05-26 19:09:34 +02:00
->eq('status', 'read')
->lt('updated', strtotime('-'.$autoflush.'day'))
->save(array('status' => 'removed', 'content' => ''));
2013-05-26 19:09:34 +02:00
}
2013-02-18 03:48:21 +01:00
}
function update_items($feed_id, array $items)
{
$nocontent = (bool) get_config_value('nocontent');
$items_in_feed = array();
2013-02-18 03:48:21 +01:00
$db = \PicoTools\singleton('db');
$db->startTransaction();
foreach ($items as $item) {
// Item parsed correctly?
if ($item->id) {
// Insert only new item
if ($db->table('items')->eq('id', $item->id)->count() !== 1) {
$db->table('items')->save(array(
'id' => $item->id,
'title' => $item->title,
'url' => $item->url,
'updated' => $item->updated,
'author' => $item->author,
2013-06-11 04:09:51 +02:00
'content' => $nocontent ? '' : $item->content,
'status' => 'unread',
'feed_id' => $feed_id
));
}
// Items inside this feed
$items_in_feed[] = $item->id;
2013-02-18 03:48:21 +01:00
}
}
// Remove from the database items marked as "removed"
// and not present inside the feed
if (! empty($items_in_feed)) {
$removed_items = \PicoTools\singleton('db')
->table('items')
->columns('id')
->notin('id', $items_in_feed)
->eq('status', 'removed')
->eq('feed_id', $feed_id)
->desc('updated')
->findAllByColumn('id');
// Keep a buffer of 2 items
// It's workaround for buggy feeds (cache issue with some Wordpress plugins)
if (is_array($removed_items)) {
$items_to_remove = array_slice($removed_items, 2);
if (! empty($items_to_remove)) {
\PicoTools\singleton('db')
->table('items')
->in('id', $items_to_remove)
->eq('status', 'removed')
->eq('feed_id', $feed_id)
->remove();
}
}
}
2013-02-18 03:48:21 +01:00
$db->closeTransaction();
}
function get_config_value($name)
{
if (! isset($_SESSION)) {
return \PicoTools\singleton('db')->table('config')->findOneColumn($name);
}
else {
if (! isset($_SESSION['config'])) {
$_SESSION['config'] = get_config();
}
if (isset($_SESSION['config'][$name])) {
return $_SESSION['config'][$name];
}
}
return null;
}
2013-02-18 03:48:21 +01:00
function get_config()
{
return \PicoTools\singleton('db')
->table('config')
2013-08-10 04:23:57 +02:00
->columns('username', 'language', 'autoflush', 'nocontent', 'items_per_page', 'theme', 'api_token', 'feed_token')
2013-02-18 03:48:21 +01:00
->findOne();
}
2013-07-04 04:05:10 +02:00
function get_user($username)
2013-02-18 03:48:21 +01:00
{
return \PicoTools\singleton('db')
->table('config')
2013-05-26 19:09:34 +02:00
->columns('username', 'password', 'language')
2013-07-04 04:05:10 +02:00
->eq('username', $username)
2013-02-18 03:48:21 +01:00
->findOne();
}
function validate_login(array $values)
{
$v = new Validator($values, array(
2013-04-13 03:08:55 +02:00
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('password', t('The password is required'))
2013-02-18 03:48:21 +01:00
));
$result = $v->execute();
$errors = $v->getErrors();
if ($result) {
2013-07-04 04:05:10 +02:00
$user = get_user($values['username']);
2013-02-18 03:48:21 +01:00
2013-03-20 05:21:48 +01:00
if ($user && \password_verify($values['password'], $user['password'])) {
2013-02-18 03:48:21 +01:00
2013-04-13 03:08:55 +02:00
unset($user['password']);
2013-02-18 03:48:21 +01:00
$_SESSION['user'] = $user;
$_SESSION['config'] = get_config();
2013-02-18 03:48:21 +01:00
}
else {
$result = false;
2013-04-13 03:08:55 +02:00
$errors['login'] = t('Bad username or password');
2013-02-18 03:48:21 +01:00
}
}
return array(
$result,
$errors
);
}
function validate_config_update(array $values)
{
if (! empty($values['password'])) {
$v = new Validator($values, array(
2013-04-13 03:08:55 +02:00
new Validators\Required('username', t('The user name is required')),
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('password', t('The password is required')),
new Validators\MinLength('password', t('The minimum length is 6 characters'), 6),
new Validators\Required('confirmation', t('The confirmation is required')),
new Validators\Equals('password', 'confirmation', t('Passwords doesn\'t match')),
new Validators\Required('autoflush', t('Value required')),
new Validators\Required('items_per_page', t('Value required')),
new Validators\Integer('items_per_page', t('Must be an integer')),
2013-07-17 03:58:11 +02:00
new Validators\Required('theme', t('Value required')),
2013-02-18 03:48:21 +01:00
));
}
else {
$v = new Validator($values, array(
2013-04-13 03:08:55 +02:00
new Validators\Required('username', t('The user name is required')),
2013-07-28 21:44:51 +02:00
new Validators\MaxLength('username', t('The maximum length is 50 characters'), 50),
new Validators\Required('autoflush', t('Value required')),
new Validators\Required('items_per_page', t('Value required')),
new Validators\Integer('items_per_page', t('Must be an integer')),
new Validators\Required('theme', t('Value required')),
2013-02-18 03:48:21 +01:00
));
}
return array(
$v->execute(),
$v->getErrors()
);
}
function save_config(array $values)
{
// Update the password if needed
2013-03-18 02:57:47 +01:00
if (! empty($values['password'])) {
2013-03-20 05:21:48 +01:00
$values['password'] = \password_hash($values['password'], PASSWORD_BCRYPT);
2013-07-17 03:58:11 +02:00
} else {
2013-03-18 02:57:47 +01:00
unset($values['password']);
}
2013-02-18 03:48:21 +01:00
unset($values['confirmation']);
// Reload configuration in session
$_SESSION['config'] = $values;
// Reload translations for flash session message
2013-04-13 03:27:51 +02:00
\PicoTools\Translator\load($values['language']);
2013-06-11 04:09:51 +02:00
// If the user does not want content of feeds, remove it in previous ones
2013-07-01 16:03:43 +02:00
if (isset($values['nocontent']) && (bool) $values['nocontent']) {
2013-06-11 04:09:51 +02:00
\PicoTools\singleton('db')->table('items')->update(array('content' => ''));
}
2013-02-18 03:48:21 +01:00
return \PicoTools\singleton('db')->table('config')->update($values);
}