2013-12-23 02:55:53 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace Model\Feed;
|
|
|
|
|
2015-04-11 16:42:23 +02:00
|
|
|
use UnexpectedValueException;
|
2014-12-24 23:54:27 +01:00
|
|
|
use Model\Config;
|
|
|
|
use Model\Item;
|
2013-12-23 02:55:53 +01:00
|
|
|
use SimpleValidator\Validator;
|
|
|
|
use SimpleValidator\Validators;
|
2014-02-08 20:13:14 +01:00
|
|
|
use PicoDb\Database;
|
2014-12-24 03:28:26 +01:00
|
|
|
use PicoFeed\Serialization\Export;
|
|
|
|
use PicoFeed\Serialization\Import;
|
|
|
|
use PicoFeed\Reader\Reader;
|
2014-12-24 23:54:27 +01:00
|
|
|
use PicoFeed\Reader\Favicon;
|
2014-12-24 03:28:26 +01:00
|
|
|
use PicoFeed\PicoFeedException;
|
2014-12-26 16:50:58 +01:00
|
|
|
use PicoFeed\Client\InvalidUrlException;
|
2013-12-23 02:55:53 +01:00
|
|
|
|
|
|
|
const LIMIT_ALL = -1;
|
|
|
|
|
2015-01-09 14:43:45 +01:00
|
|
|
// Store the favicon
|
|
|
|
function store_favicon($feed_id, $link, $icon)
|
2014-12-24 23:54:27 +01:00
|
|
|
{
|
|
|
|
return Database::get('db')
|
|
|
|
->table('favicons')
|
|
|
|
->save(array(
|
|
|
|
'feed_id' => $feed_id,
|
2015-01-09 14:43:45 +01:00
|
|
|
'link' => $link,
|
|
|
|
'icon' => $icon,
|
2014-12-24 23:54:27 +01:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
2015-01-09 14:43:45 +01:00
|
|
|
// Download favicon
|
2015-01-18 15:20:36 +01:00
|
|
|
function fetch_favicon($feed_id, $site_url, $icon_link)
|
2014-12-24 23:54:27 +01:00
|
|
|
{
|
|
|
|
if (Config\get('favicons') == 1 && ! has_favicon($feed_id)) {
|
2015-01-09 14:43:45 +01:00
|
|
|
$favicon = new Favicon;
|
|
|
|
|
2015-01-18 15:20:36 +01:00
|
|
|
$link = $favicon->find($site_url, $icon_link);
|
2015-01-09 14:43:45 +01:00
|
|
|
$icon = $favicon->getDataUri();
|
|
|
|
|
|
|
|
if ($icon !== '') {
|
|
|
|
store_favicon($feed_id, $link, $icon);
|
|
|
|
}
|
2014-12-24 23:54:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return true if the feed have a favicon
|
|
|
|
function has_favicon($feed_id)
|
|
|
|
{
|
|
|
|
return Database::get('db')->table('favicons')->eq('feed_id', $feed_id)->count() === 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get favicons for those feeds
|
|
|
|
function get_favicons(array $feed_ids)
|
|
|
|
{
|
|
|
|
if (Config\get('favicons') == 0) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
2015-01-22 23:12:06 +01:00
|
|
|
$db = Database::get('db')
|
2015-01-28 02:13:16 +01:00
|
|
|
->hashtable('favicons')
|
2015-01-22 23:12:06 +01:00
|
|
|
->columnKey('feed_id')
|
|
|
|
->columnValue('icon');
|
|
|
|
|
|
|
|
// pass $feeds_ids as argument list to hashtable::get(), use ... operator with php 5.6+
|
|
|
|
return call_user_func_array(array($db, 'get'), $feed_ids);
|
2014-12-24 23:54:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get all favicons for a list of items
|
|
|
|
function get_item_favicons(array $items)
|
|
|
|
{
|
|
|
|
$feed_ids = array();
|
|
|
|
|
|
|
|
foreach ($items as $item) {
|
2015-01-22 23:12:06 +01:00
|
|
|
$feed_ids[$item['feed_id']] = $item['feed_id'];
|
2014-12-24 23:54:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return get_favicons($feed_ids);
|
|
|
|
}
|
|
|
|
|
2014-12-25 00:12:41 +01:00
|
|
|
// Get all favicons
|
|
|
|
function get_all_favicons()
|
|
|
|
{
|
|
|
|
if (Config\get('favicons') == 0) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
return Database::get('db')
|
2015-01-28 02:13:16 +01:00
|
|
|
->hashtable('favicons')
|
|
|
|
->getAll('feed_id', 'icon');
|
2014-12-25 00:12:41 +01:00
|
|
|
}
|
|
|
|
|
2013-12-23 02:55:53 +01:00
|
|
|
// Update feed information
|
|
|
|
function update(array $values)
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
return Database::get('db')
|
2013-12-23 02:55:53 +01:00
|
|
|
->table('feeds')
|
|
|
|
->eq('id', $values['id'])
|
|
|
|
->save(array(
|
|
|
|
'title' => $values['title'],
|
|
|
|
'site_url' => $values['site_url'],
|
2014-10-20 01:14:33 +02:00
|
|
|
'feed_url' => $values['feed_url'],
|
2015-04-11 23:05:57 +02:00
|
|
|
'enabled' => $values['enabled'],
|
|
|
|
'rtl' => $values['rtl'],
|
|
|
|
'download_content' => $values['download_content'],
|
|
|
|
'cloak_referrer' => $values['cloak_referrer'],
|
2013-12-23 02:55:53 +01:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Export all feeds
|
|
|
|
function export_opml()
|
|
|
|
{
|
2014-05-20 20:20:27 +02:00
|
|
|
$opml = new Export(get_all());
|
2013-12-23 02:55:53 +01:00
|
|
|
return $opml->execute();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Import OPML file
|
|
|
|
function import_opml($content)
|
|
|
|
{
|
2014-05-20 20:20:27 +02:00
|
|
|
$import = new Import($content);
|
2013-12-23 02:55:53 +01:00
|
|
|
$feeds = $import->execute();
|
|
|
|
|
|
|
|
if ($feeds) {
|
|
|
|
|
2014-02-08 20:13:14 +01:00
|
|
|
$db = Database::get('db');
|
2013-12-23 02:55:53 +01:00
|
|
|
$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();
|
|
|
|
|
2014-05-20 20:20:27 +02:00
|
|
|
Config\write_debug();
|
2013-12-23 02:55:53 +01:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2014-05-20 20:20:27 +02:00
|
|
|
Config\write_debug();
|
2013-12-23 02:55:53 +01:00
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add a new feed from an URL
|
2015-02-01 22:54:57 +01:00
|
|
|
function create($url, $enable_grabber = false, $force_rtl = false, $cloak_referrer = false)
|
2013-12-23 02:55:53 +01:00
|
|
|
{
|
2015-04-10 23:34:07 +02:00
|
|
|
$feed_id = false;
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2015-04-10 23:34:07 +02:00
|
|
|
$db = Database::get('db');
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2015-04-10 23:34:07 +02:00
|
|
|
// Discover the feed
|
|
|
|
$reader = new Reader(Config\get_reader_config());
|
|
|
|
$resource = $reader->discover($url);
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2015-04-10 23:34:07 +02:00
|
|
|
// Feed already there
|
|
|
|
if ($db->table('feeds')->eq('feed_url', $resource->getUrl())->count()) {
|
2015-04-11 16:42:23 +02:00
|
|
|
throw new UnexpectedValueException;
|
2015-04-10 23:34:07 +02:00
|
|
|
}
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2015-04-10 23:34:07 +02:00
|
|
|
// Parse the feed
|
|
|
|
$parser = $reader->getParser(
|
|
|
|
$resource->getUrl(),
|
|
|
|
$resource->getContent(),
|
|
|
|
$resource->getEncoding()
|
|
|
|
);
|
2014-12-24 23:54:27 +01:00
|
|
|
|
2015-04-10 23:34:07 +02:00
|
|
|
if ($enable_grabber) {
|
|
|
|
$parser->enableContentGrabber();
|
|
|
|
}
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2015-04-10 23:34:07 +02:00
|
|
|
$feed = $parser->execute();
|
|
|
|
|
|
|
|
// Save the feed
|
|
|
|
$result = $db->table('feeds')->save(array(
|
|
|
|
'title' => $feed->getTitle(),
|
|
|
|
'site_url' => $feed->getSiteUrl(),
|
|
|
|
'feed_url' => $feed->getFeedUrl(),
|
|
|
|
'download_content' => $enable_grabber ? 1 : 0,
|
|
|
|
'rtl' => $force_rtl ? 1 : 0,
|
|
|
|
'last_modified' => $resource->getLastModified(),
|
|
|
|
'last_checked' => time(),
|
|
|
|
'etag' => $resource->getEtag(),
|
|
|
|
'cloak_referrer' => $cloak_referrer ? 1 : 0,
|
|
|
|
));
|
|
|
|
|
|
|
|
if ($result) {
|
|
|
|
$feed_id = $db->getConnection()->getLastId();
|
|
|
|
|
|
|
|
Item\update_all($feed_id, $feed->getItems());
|
|
|
|
fetch_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
2015-04-10 23:34:07 +02:00
|
|
|
return $feed_id;
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh all feeds
|
|
|
|
function refresh_all($limit = LIMIT_ALL)
|
|
|
|
{
|
2014-12-24 03:28:26 +01:00
|
|
|
foreach (@get_ids($limit) as $feed_id) {
|
2013-12-23 02:55:53 +01:00
|
|
|
refresh($feed_id);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Auto-vacuum for people using the cronjob
|
2014-02-08 20:13:14 +01:00
|
|
|
Database::get('db')->getConnection()->exec('VACUUM');
|
2013-12-23 02:55:53 +01:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refresh one feed
|
|
|
|
function refresh($feed_id)
|
|
|
|
{
|
2014-12-24 03:28:26 +01:00
|
|
|
try {
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
$feed = get($feed_id);
|
2014-05-20 20:20:27 +02:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
if (empty($feed)) {
|
|
|
|
return false;
|
|
|
|
}
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
$reader = new Reader(Config\get_reader_config());
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
$resource = $reader->download(
|
|
|
|
$feed['feed_url'],
|
|
|
|
$feed['last_modified'],
|
|
|
|
$feed['etag']
|
|
|
|
);
|
2014-02-23 01:45:02 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
// Update the `last_checked` column each time, HTTP cache or not
|
|
|
|
update_last_checked($feed_id);
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
// Feed modified
|
|
|
|
if ($resource->isModified()) {
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
$parser = $reader->getParser(
|
|
|
|
$resource->getUrl(),
|
|
|
|
$resource->getContent(),
|
|
|
|
$resource->getEncoding()
|
|
|
|
);
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
if ($feed['download_content']) {
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
$parser->enableContentGrabber();
|
2013-12-23 02:55:53 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
// Don't fetch previous items, only new one
|
|
|
|
$parser->setGrabberIgnoreUrls(
|
|
|
|
Database::get('db')->table('items')->eq('feed_id', $feed_id)->findAllByColumn('url')
|
|
|
|
);
|
|
|
|
}
|
2014-02-23 01:45:02 +01:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
$feed = $parser->execute();
|
2013-12-23 02:55:53 +01:00
|
|
|
|
|
|
|
update_cache($feed_id, $resource->getLastModified(), $resource->getEtag());
|
2014-05-20 20:20:27 +02:00
|
|
|
|
2014-12-24 03:28:26 +01:00
|
|
|
Item\update_all($feed_id, $feed->getItems());
|
2015-01-18 15:20:36 +01:00
|
|
|
fetch_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
2014-12-24 03:28:26 +01:00
|
|
|
|
|
|
|
update_parsing_error($feed_id, 0);
|
|
|
|
Config\write_debug();
|
|
|
|
|
|
|
|
return true;
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
2014-12-26 16:50:58 +01:00
|
|
|
catch (InvalidUrlException $e) {
|
2014-12-30 00:49:49 +01:00
|
|
|
// disable($feed_id);
|
2014-12-26 16:50:58 +01:00
|
|
|
}
|
|
|
|
catch (PicoFeedException $e) {
|
|
|
|
}
|
2013-12-23 02:55:53 +01:00
|
|
|
|
|
|
|
update_parsing_error($feed_id, 1);
|
2014-05-20 20:20:27 +02:00
|
|
|
Config\write_debug();
|
2013-12-23 02:55:53 +01:00
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get the list of feeds ID to refresh
|
|
|
|
function get_ids($limit = LIMIT_ALL)
|
|
|
|
{
|
2014-12-24 03:28:26 +01:00
|
|
|
$query = Database::get('db')->table('feeds')->eq('enabled', 1)->asc('last_checked');
|
2013-12-23 02:55:53 +01:00
|
|
|
|
|
|
|
if ($limit !== LIMIT_ALL) {
|
2014-12-24 03:28:26 +01:00
|
|
|
$query->limit((int) $limit);
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
2015-01-28 02:13:16 +01:00
|
|
|
return $query->findAllByColumn('id');
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
2015-01-14 19:44:41 +01:00
|
|
|
// get number of feeds with errors
|
|
|
|
function count_failed_feeds()
|
2013-12-23 02:55:53 +01:00
|
|
|
{
|
2015-01-14 19:44:41 +01:00
|
|
|
return Database::get('db')
|
2013-12-23 02:55:53 +01:00
|
|
|
->table('feeds')
|
2015-01-14 19:44:41 +01:00
|
|
|
->eq('parsing_error', '1')
|
|
|
|
->count();
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Get all feeds
|
|
|
|
function get_all()
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
return Database::get('db')
|
2013-12-23 02:55:53 +01:00
|
|
|
->table('feeds')
|
|
|
|
->asc('title')
|
|
|
|
->findAll();
|
|
|
|
}
|
|
|
|
|
2015-01-14 19:44:41 +01:00
|
|
|
// Get all feeds with the number unread/total items in the order failed, working, disabled
|
2014-02-23 01:45:02 +01:00
|
|
|
function get_all_item_counts()
|
2014-02-22 20:00:49 +01:00
|
|
|
{
|
2015-01-14 19:44:41 +01:00
|
|
|
return Database::get('db')
|
2014-02-23 01:45:02 +01:00
|
|
|
->table('feeds')
|
2015-01-14 19:44:41 +01:00
|
|
|
->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')
|
|
|
|
->groupBy('feeds.id')
|
|
|
|
->desc('feeds.parsing_error')
|
|
|
|
->desc('feeds.enabled')
|
|
|
|
->asc('feeds.title')
|
2014-02-23 01:45:02 +01:00
|
|
|
->findAll();
|
2014-02-22 20:00:49 +01:00
|
|
|
}
|
|
|
|
|
2014-02-23 01:45:02 +01:00
|
|
|
// Get unread/total count for one feed
|
|
|
|
function count_items($feed_id)
|
2014-02-22 20:00:49 +01:00
|
|
|
{
|
2014-02-23 01:45:02 +01:00
|
|
|
$counts = Database::get('db')
|
|
|
|
->table('items')
|
|
|
|
->columns('status', 'count(*) as item_count')
|
|
|
|
->in('status', array('read', 'unread'))
|
|
|
|
->eq('feed_id', $feed_id)
|
|
|
|
->groupBy('status')
|
|
|
|
->findAll();
|
|
|
|
|
|
|
|
$result = array(
|
|
|
|
'items_unread' => 0,
|
|
|
|
'items_total' => 0,
|
|
|
|
);
|
|
|
|
|
|
|
|
foreach ($counts as &$count) {
|
|
|
|
|
|
|
|
if ($count['status'] === 'unread') {
|
|
|
|
$result['items_unread'] = (int) $count['item_count'];
|
2014-02-22 20:00:49 +01:00
|
|
|
}
|
2014-02-23 01:45:02 +01:00
|
|
|
|
|
|
|
$result['items_total'] += $count['item_count'];
|
2014-02-22 20:00:49 +01:00
|
|
|
}
|
2014-02-23 01:45:02 +01:00
|
|
|
|
|
|
|
return $result;
|
2014-02-22 20:00:49 +01:00
|
|
|
}
|
|
|
|
|
2013-12-23 02:55:53 +01:00
|
|
|
// Get one feed
|
|
|
|
function get($feed_id)
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
return Database::get('db')
|
2013-12-23 02:55:53 +01:00
|
|
|
->table('feeds')
|
|
|
|
->eq('id', $feed_id)
|
|
|
|
->findOne();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update parsing error column
|
|
|
|
function update_parsing_error($feed_id, $value)
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
Database::get('db')->table('feeds')->eq('id', $feed_id)->save(array('parsing_error' => $value));
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update last check date
|
|
|
|
function update_last_checked($feed_id)
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
Database::get('db')
|
2013-12-23 02:55:53 +01:00
|
|
|
->table('feeds')
|
|
|
|
->eq('id', $feed_id)
|
|
|
|
->save(array(
|
|
|
|
'last_checked' => time()
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update Etag and last Modified columns
|
|
|
|
function update_cache($feed_id, $last_modified, $etag)
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
Database::get('db')
|
2013-12-23 02:55:53 +01:00
|
|
|
->table('feeds')
|
|
|
|
->eq('id', $feed_id)
|
|
|
|
->save(array(
|
|
|
|
'last_modified' => $last_modified,
|
|
|
|
'etag' => $etag
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove one feed
|
|
|
|
function remove($feed_id)
|
|
|
|
{
|
|
|
|
// Items are removed by a sql constraint
|
2014-02-08 20:13:14 +01:00
|
|
|
return Database::get('db')->table('feeds')->eq('id', $feed_id)->remove();
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove all feeds
|
|
|
|
function remove_all()
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
return Database::get('db')->table('feeds')->remove();
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Enable a feed (activate refresh)
|
|
|
|
function enable($feed_id)
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
return Database::get('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 1)));
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Disable feed
|
|
|
|
function disable($feed_id)
|
|
|
|
{
|
2014-02-08 20:13:14 +01:00
|
|
|
return Database::get('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 0)));
|
2013-12-23 02:55:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validation for edit
|
|
|
|
function validate_modification(array $values)
|
|
|
|
{
|
|
|
|
$v = new Validator($values, array(
|
|
|
|
new Validators\Required('id', t('The feed id is required')),
|
|
|
|
new Validators\Required('title', t('The title is required')),
|
|
|
|
new Validators\Required('site_url', t('The site url is required')),
|
|
|
|
new Validators\Required('feed_url', t('The feed url is required')),
|
|
|
|
));
|
|
|
|
|
|
|
|
$result = $v->execute();
|
|
|
|
$errors = $v->getErrors();
|
|
|
|
|
|
|
|
return array(
|
|
|
|
$result,
|
|
|
|
$errors
|
|
|
|
);
|
|
|
|
}
|