c35dd27f01
Main purpose is to prevent orphaned groups. It's not possible to use the ON DELETE CASCADE trigger here, to remove the group together with the last feed that is assigned to this group. The ON DELETE CASCADE trigger will raise an foreign key violation error in cases where the removed feed is not the last feed associated to a group. The purge_groups() call has been moved to the remove group functions, since it's the only way to create an orphaned group.
481 lines
12 KiB
PHP
481 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Model\Feed;
|
|
|
|
use UnexpectedValueException;
|
|
use Model\Config;
|
|
use Model\Item;
|
|
use Model\Group;
|
|
use Helper;
|
|
use SimpleValidator\Validator;
|
|
use SimpleValidator\Validators;
|
|
use PicoDb\Database;
|
|
use PicoFeed\Serialization\Export;
|
|
use PicoFeed\Serialization\Import;
|
|
use PicoFeed\Reader\Reader;
|
|
use PicoFeed\Reader\Favicon;
|
|
use PicoFeed\PicoFeedException;
|
|
use PicoFeed\Client\InvalidUrlException;
|
|
|
|
const LIMIT_ALL = -1;
|
|
|
|
// Store the favicon
|
|
function store_favicon($feed_id, $link, $type, $icon)
|
|
{
|
|
$file = $feed_id.Helper\favicon_extension($type);
|
|
|
|
if (file_put_contents(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$file, $icon) === false) {
|
|
return false;
|
|
}
|
|
|
|
return Database::getInstance('db')
|
|
->table('favicons')
|
|
->save(array(
|
|
'feed_id' => $feed_id,
|
|
'link' => $link,
|
|
'file' => $file,
|
|
'type' => $type
|
|
));
|
|
}
|
|
|
|
// Delete the favicon
|
|
function delete_favicon($feed_id)
|
|
{
|
|
foreach (get_favicons(array ($feed_id)) as $favicon) {
|
|
unlink(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$favicon);
|
|
}
|
|
}
|
|
|
|
// Delete all the favicons
|
|
function delete_all_favicons()
|
|
{
|
|
foreach (get_all_favicons() as $favicon) {
|
|
unlink(FAVICON_DIRECTORY.DIRECTORY_SEPARATOR.$favicon);
|
|
}
|
|
}
|
|
|
|
// Download favicon
|
|
function fetch_favicon($feed_id, $site_url, $icon_link)
|
|
{
|
|
if (Config\get('favicons') == 1 && ! has_favicon($feed_id)) {
|
|
$favicon = new Favicon;
|
|
|
|
$link = $favicon->find($site_url, $icon_link);
|
|
$icon = $favicon->getContent();
|
|
$type = $favicon->getType();
|
|
|
|
if ($icon !== '') {
|
|
store_favicon($feed_id, $link, $type, $icon);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return true if the feed have a favicon
|
|
function has_favicon($feed_id)
|
|
{
|
|
return Database::getInstance('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();
|
|
}
|
|
|
|
$db = Database::getInstance('db')
|
|
->hashtable('favicons')
|
|
->columnKey('feed_id')
|
|
->columnValue('file');
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Get all favicons for a list of items
|
|
function get_item_favicons(array $items)
|
|
{
|
|
$feed_ids = array();
|
|
|
|
foreach ($items as $item) {
|
|
$feed_ids[$item['feed_id']] = $item['feed_id'];
|
|
}
|
|
|
|
return get_favicons($feed_ids);
|
|
}
|
|
|
|
// Get all favicons
|
|
function get_all_favicons()
|
|
{
|
|
if (Config\get('favicons') == 0) {
|
|
return array();
|
|
}
|
|
|
|
return Database::getInstance('db')
|
|
->hashtable('favicons')
|
|
->getAll('feed_id', 'file');
|
|
}
|
|
|
|
// Update feed information
|
|
function update(array $values)
|
|
{
|
|
Database::getInstance('db')->startTransaction();
|
|
|
|
$result = Database::getInstance('db')
|
|
->table('feeds')
|
|
->eq('id', $values['id'])
|
|
->save(array(
|
|
'title' => $values['title'],
|
|
'site_url' => $values['site_url'],
|
|
'feed_url' => $values['feed_url'],
|
|
'enabled' => $values['enabled'],
|
|
'rtl' => $values['rtl'],
|
|
'download_content' => $values['download_content'],
|
|
'cloak_referrer' => $values['cloak_referrer'],
|
|
));
|
|
|
|
if ($result) {
|
|
if (! Group\update_feed_groups($values['id'], $values['feed_group_ids'], $values['create_group'])) {
|
|
Database::getInstance('db')->cancelTransaction();
|
|
$result = false;
|
|
}
|
|
}
|
|
|
|
Database::getInstance('db')->closeTransaction();
|
|
|
|
return $result;
|
|
}
|
|
|
|
// Export all feeds
|
|
function export_opml()
|
|
{
|
|
$opml = new Export(get_all());
|
|
return $opml->execute();
|
|
}
|
|
|
|
// Import OPML file
|
|
function import_opml($content)
|
|
{
|
|
$import = new Import($content);
|
|
$feeds = $import->execute();
|
|
|
|
if ($feeds) {
|
|
|
|
$db = Database::getInstance('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();
|
|
|
|
Config\write_debug();
|
|
|
|
return true;
|
|
}
|
|
|
|
Config\write_debug();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Add a new feed from an URL
|
|
function create($url, $enable_grabber = false, $force_rtl = false, $cloak_referrer = false, $group_ids = array(), $create_group = '')
|
|
{
|
|
$feed_id = false;
|
|
|
|
$db = Database::getInstance('db');
|
|
|
|
// Discover the feed
|
|
$reader = new Reader(Config\get_reader_config());
|
|
$resource = $reader->discover($url);
|
|
|
|
// Feed already there
|
|
if ($db->table('feeds')->eq('feed_url', $resource->getUrl())->count()) {
|
|
throw new UnexpectedValueException;
|
|
}
|
|
|
|
// Parse the feed
|
|
$parser = $reader->getParser(
|
|
$resource->getUrl(),
|
|
$resource->getContent(),
|
|
$resource->getEncoding()
|
|
);
|
|
|
|
if ($enable_grabber) {
|
|
$parser->enableContentGrabber();
|
|
}
|
|
|
|
$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->getLastId();
|
|
|
|
Group\update_feed_groups($feed_id, $group_ids, $create_group);
|
|
Item\update_all($feed_id, $feed->getItems());
|
|
fetch_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
|
|
}
|
|
|
|
return $feed_id;
|
|
}
|
|
|
|
// Refresh all feeds
|
|
function refresh_all($limit = LIMIT_ALL)
|
|
{
|
|
foreach (get_ids($limit) as $feed_id) {
|
|
refresh($feed_id);
|
|
}
|
|
|
|
// Auto-vacuum for people using the cronjob
|
|
Database::getInstance('db')->getConnection()->exec('VACUUM');
|
|
|
|
return true;
|
|
}
|
|
|
|
// Refresh one feed
|
|
function refresh($feed_id)
|
|
{
|
|
try {
|
|
|
|
$feed = get($feed_id);
|
|
|
|
if (empty($feed)) {
|
|
return false;
|
|
}
|
|
|
|
$reader = new Reader(Config\get_reader_config());
|
|
|
|
$resource = $reader->download(
|
|
$feed['feed_url'],
|
|
$feed['last_modified'],
|
|
$feed['etag']
|
|
);
|
|
|
|
// Update the `last_checked` column each time, HTTP cache or not
|
|
update_last_checked($feed_id);
|
|
|
|
// Feed modified
|
|
if ($resource->isModified()) {
|
|
|
|
$parser = $reader->getParser(
|
|
$resource->getUrl(),
|
|
$resource->getContent(),
|
|
$resource->getEncoding()
|
|
);
|
|
|
|
if ($feed['download_content']) {
|
|
|
|
$parser->enableContentGrabber();
|
|
|
|
// Don't fetch previous items, only new one
|
|
$parser->setGrabberIgnoreUrls(
|
|
Database::getInstance('db')->table('items')->eq('feed_id', $feed_id)->findAllByColumn('url')
|
|
);
|
|
}
|
|
|
|
$feed = $parser->execute();
|
|
|
|
update_cache($feed_id, $resource->getLastModified(), $resource->getEtag());
|
|
|
|
Item\update_all($feed_id, $feed->getItems());
|
|
fetch_favicon($feed_id, $feed->getSiteUrl(), $feed->getIcon());
|
|
}
|
|
|
|
update_parsing_error($feed_id, 0);
|
|
Config\write_debug();
|
|
|
|
return true;
|
|
}
|
|
catch (PicoFeedException $e) {
|
|
}
|
|
|
|
update_parsing_error($feed_id, 1);
|
|
Config\write_debug();
|
|
|
|
return false;
|
|
}
|
|
|
|
// Get the list of feeds ID to refresh
|
|
function get_ids($limit = LIMIT_ALL)
|
|
{
|
|
$query = Database::getInstance('db')->table('feeds')->eq('enabled', 1)->asc('last_checked');
|
|
|
|
if ($limit !== LIMIT_ALL) {
|
|
$query->limit((int) $limit);
|
|
}
|
|
|
|
return $query->findAllByColumn('id');
|
|
}
|
|
|
|
// get number of feeds with errors
|
|
function count_failed_feeds()
|
|
{
|
|
return Database::getInstance('db')
|
|
->table('feeds')
|
|
->eq('parsing_error', '1')
|
|
->count();
|
|
}
|
|
|
|
// Get all feeds
|
|
function get_all()
|
|
{
|
|
return Database::getInstance('db')
|
|
->table('feeds')
|
|
->asc('title')
|
|
->findAll();
|
|
}
|
|
|
|
// Get all feeds with the number unread/total items in the order failed, working, disabled
|
|
function get_all_item_counts()
|
|
{
|
|
return Database::getInstance('db')
|
|
->table('feeds')
|
|
->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')
|
|
->findAll();
|
|
}
|
|
|
|
// Get unread/total count for one feed
|
|
function count_items($feed_id)
|
|
{
|
|
$counts = Database::getInstance('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'];
|
|
}
|
|
|
|
$result['items_total'] += $count['item_count'];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
// Get one feed
|
|
function get($feed_id)
|
|
{
|
|
return Database::getInstance('db')
|
|
->table('feeds')
|
|
->eq('id', $feed_id)
|
|
->findOne();
|
|
}
|
|
|
|
// Update parsing error column
|
|
function update_parsing_error($feed_id, $value)
|
|
{
|
|
Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save(array('parsing_error' => $value));
|
|
}
|
|
|
|
// Update last check date
|
|
function update_last_checked($feed_id)
|
|
{
|
|
Database::getInstance('db')
|
|
->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)
|
|
{
|
|
Database::getInstance('db')
|
|
->table('feeds')
|
|
->eq('id', $feed_id)
|
|
->save(array(
|
|
'last_modified' => $last_modified,
|
|
'etag' => $etag
|
|
));
|
|
}
|
|
|
|
// Remove one feed
|
|
function remove($feed_id)
|
|
{
|
|
delete_favicon($feed_id);
|
|
Group\remove_all($feed_id);
|
|
|
|
// Items are removed by a sql constraint
|
|
return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->remove();
|
|
}
|
|
|
|
// Remove all feeds
|
|
function remove_all()
|
|
{
|
|
delete_all_favicons();
|
|
return Database::getInstance('db')->table('feeds')->remove();
|
|
}
|
|
|
|
// Enable a feed (activate refresh)
|
|
function enable($feed_id)
|
|
{
|
|
return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 1)));
|
|
}
|
|
|
|
// Disable feed
|
|
function disable($feed_id)
|
|
{
|
|
return Database::getInstance('db')->table('feeds')->eq('id', $feed_id)->save((array('enabled' => 0)));
|
|
}
|
|
|
|
// 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
|
|
);
|
|
}
|