miniflux-legacy/models/feed.php
Mathias Kresin c35dd27f01 remove all group from feeds before deleting
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.
2015-12-13 16:14:55 +01:00

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
);
}