410 lines
12 KiB
C
410 lines
12 KiB
C
// This file is part of Snownews - A lightweight console RSS newsreader
|
|
//
|
|
// Copyright (c) 2003-2004 Oliver Feiler <kiza@kcore.de>
|
|
// Copyright (c) 2021 Mike Sharov <msharov@users.sourceforge.net>
|
|
//
|
|
// Snownews is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 3
|
|
// as published by the Free Software Foundation.
|
|
//
|
|
// Snownews is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty
|
|
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
// See the GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Snownews. If not, see http://www.gnu.org/licenses/.
|
|
|
|
#include "feedio.h"
|
|
#include "conv.h"
|
|
#include "filters.h"
|
|
#include "netio.h"
|
|
#include "uiutil.h"
|
|
#include "parse.h"
|
|
#include "setup.h"
|
|
#include "cat.h"
|
|
#include <ncurses.h>
|
|
#include <libxml/parser.h>
|
|
|
|
struct feed* newFeedStruct (void)
|
|
{
|
|
return calloc (1, sizeof (struct feed));
|
|
}
|
|
|
|
// Update given feed from server.
|
|
// Reload XML document and replace in memory cur_ptr->xmltext with it.
|
|
int UpdateFeed (struct feed* cur_ptr)
|
|
{
|
|
if (cur_ptr == NULL)
|
|
return 1;
|
|
|
|
// Smart feeds are generated in the fly.
|
|
if (cur_ptr->smartfeed == 1)
|
|
return 0;
|
|
|
|
char stbuf [128];
|
|
snprintf (stbuf, sizeof(stbuf), _("Downloading %s"), cur_ptr->title ? cur_ptr->title : _("feed"));
|
|
UIStatus (stbuf, 0, 0);
|
|
|
|
// If current feed is an exec URL, run that command, otherwise fetch resource from network.
|
|
if (cur_ptr->execurl)
|
|
FilterExecURL (cur_ptr);
|
|
else {
|
|
DownloadFeed (cur_ptr->feedurl, cur_ptr);
|
|
|
|
// Set title and link structure to something.
|
|
// To the feedurl in this case so the program show something
|
|
// as placeholder instead of crash.
|
|
if (cur_ptr->title == NULL)
|
|
cur_ptr->title = strdup (cur_ptr->feedurl);
|
|
if (cur_ptr->link == NULL)
|
|
cur_ptr->link = strdup (cur_ptr->feedurl);
|
|
|
|
// If the download function returns a NULL pointer return from here.
|
|
if (!cur_ptr->xmltext)
|
|
return 1;
|
|
}
|
|
|
|
// Feed downloaded content through the defined filter.
|
|
if (cur_ptr->perfeedfilter != NULL)
|
|
FilterPipeNG (cur_ptr);
|
|
|
|
// If there is no feed, return.
|
|
if (!cur_ptr->xmltext)
|
|
return 1;
|
|
|
|
if (DeXML (cur_ptr)) {
|
|
UIStatus (_("Invalid XML! Cannot parse this feed!"), 2, 1);
|
|
// Activate feed problem flag.
|
|
cur_ptr->problem = true;
|
|
return 1;
|
|
}
|
|
// We don't need these anymore. Free the raw XML to save some memory.
|
|
free (cur_ptr->xmltext);
|
|
cur_ptr->xmltext = NULL;
|
|
cur_ptr->content_length = 0;
|
|
|
|
// Mark the time to detect modifications
|
|
cur_ptr->mtime = time (NULL);
|
|
return 0;
|
|
}
|
|
|
|
int UpdateAllFeeds (void)
|
|
{
|
|
for (struct feed* f = _feed_list; f; f = f->next)
|
|
if (0 != UpdateFeed (f))
|
|
continue;
|
|
return 0;
|
|
}
|
|
|
|
// Load feed from disk. And call UpdateFeed if neccessary.
|
|
int LoadFeed (struct feed* cur_ptr)
|
|
{
|
|
// Smart feeds are generated in the fly.
|
|
if (cur_ptr->smartfeed == 1)
|
|
return 0;
|
|
|
|
char* hashme = Hashify (cur_ptr->feedurl);
|
|
char cachefilename [PATH_MAX];
|
|
CacheFilePath (hashme, cachefilename, sizeof(cachefilename));
|
|
free (hashme);
|
|
FILE* cache = fopen (cachefilename, "r");
|
|
if (cache == NULL) {
|
|
char msgbuf[128];
|
|
snprintf (msgbuf, sizeof (msgbuf), _("Cache for %s is toast. Reloading from server..."), cur_ptr->feedurl);
|
|
UIStatus (msgbuf, 0, 0);
|
|
|
|
if (UpdateFeed (cur_ptr) != 0)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
struct stat cachest;
|
|
fstat (fileno (cache), &cachest);
|
|
|
|
// Read complete cachefile.
|
|
int len = 0; // Internal usage for realloc.
|
|
char filebuf[BUFSIZ]; // File I/O block buffer.
|
|
while (!feof (cache)) {
|
|
// Use binary read, UTF-8 data!
|
|
size_t retval = fread (filebuf, 1, sizeof (filebuf), cache);
|
|
if (retval == 0)
|
|
break;
|
|
cur_ptr->xmltext = realloc (cur_ptr->xmltext, len + retval + 1);
|
|
memcpy (cur_ptr->xmltext + len, filebuf, retval);
|
|
len += retval;
|
|
if (retval != sizeof (filebuf))
|
|
break;
|
|
}
|
|
fclose (cache);
|
|
cur_ptr->content_length = len;
|
|
cur_ptr->xmltext[len] = '\0';
|
|
|
|
if (!cur_ptr->xmltext)
|
|
return 1;
|
|
|
|
// After loading DeXMLize the mess.
|
|
// If loading the feed from the disk fails, try downloading from the net.
|
|
if (DeXML (cur_ptr) != 0 && UpdateFeed (cur_ptr) != 0) {
|
|
// And if that fails as well, just continue without this feed.
|
|
char msgbuf[64];
|
|
snprintf (msgbuf, sizeof (msgbuf), _("Could not load %s!"), cur_ptr->feedurl);
|
|
UIStatus (msgbuf, 2, 1);
|
|
}
|
|
|
|
free (cur_ptr->xmltext);
|
|
cur_ptr->xmltext = NULL;
|
|
cur_ptr->content_length = 0;
|
|
cur_ptr->mtime = cachest.st_mtime;
|
|
return 0;
|
|
}
|
|
|
|
int LoadAllFeeds (unsigned numfeeds)
|
|
{
|
|
if (!numfeeds)
|
|
return 0;
|
|
UIStatus (_("Loading cache ["), 0, 0);
|
|
unsigned titlestrlen = strlen (_("Loading cache ["));
|
|
int oldnumobjects = 0;
|
|
unsigned count = 1;
|
|
for (struct feed* f = _feed_list; f; f = f->next) {
|
|
// Progress bar
|
|
int numobjects = count * (COLS - titlestrlen - 2) / numfeeds - 2;
|
|
if (numobjects < 1)
|
|
numobjects = 1;
|
|
if (numobjects > oldnumobjects) {
|
|
DrawProgressBar (numobjects, titlestrlen);
|
|
oldnumobjects = numobjects;
|
|
}
|
|
if (LoadFeed (f) != 0)
|
|
continue;
|
|
++count;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void AddFeedToList (struct feed* new_feed)
|
|
{
|
|
if (!_feed_list)
|
|
_feed_list = new_feed;
|
|
else {
|
|
new_feed->prev = _feed_list;
|
|
while (new_feed->prev->next)
|
|
new_feed->prev = new_feed->prev->next;
|
|
new_feed->prev->next = new_feed;
|
|
}
|
|
}
|
|
|
|
void AddFeed (const char* url, const char* cname, const char* categories, const char* filter)
|
|
{
|
|
struct feed* new_ptr = newFeedStruct();
|
|
new_ptr->feedurl = strdup (url);
|
|
if (strncasecmp (new_ptr->feedurl, "exec:", strlen ("exec:")) == 0)
|
|
new_ptr->execurl = true;
|
|
else if (strncasecmp (new_ptr->feedurl, "smartfeed:", strlen ("smartfeed:")) == 0)
|
|
new_ptr->smartfeed = true;
|
|
if (cname && cname[0])
|
|
new_ptr->custom_title = strdup (cname);
|
|
if (filter && filter[0])
|
|
new_ptr->perfeedfilter = strdup (filter);
|
|
if (categories && categories[0]) { // Put categories into cat struct.
|
|
char* catlist = strdup (categories);
|
|
for (char* catnext = catlist; catnext;)
|
|
FeedCategoryAdd (new_ptr, strsep (&catnext, ","));
|
|
free (catlist);
|
|
}
|
|
AddFeedToList (new_ptr);
|
|
}
|
|
|
|
static void WriteFeedUrls (void)
|
|
{
|
|
if (!_feed_list_changed)
|
|
return;
|
|
|
|
// Make a backup of urls.
|
|
char urlsfilename[PATH_MAX];
|
|
ConfigFilePath ("urls.opml", urlsfilename, sizeof(urlsfilename));
|
|
|
|
// Write urls
|
|
FILE* urlfile = fopen (urlsfilename, "w");
|
|
if (!urlfile) {
|
|
syslog (LOG_ERR, "error saving urls: %s", strerror (errno));
|
|
return;
|
|
}
|
|
|
|
// Write header
|
|
fputs (
|
|
"<?xml version=\"1.0\"?>\n"
|
|
"<opml version=\"2.0\"", urlfile);
|
|
|
|
// See if the custom namespace is needed
|
|
bool needNs = false;
|
|
for (const struct feed* f = _feed_list; f; f = f->next)
|
|
if (f->perfeedfilter)
|
|
needNs = true;
|
|
if (needNs)
|
|
fputs (" xmlns:snow=\"http://snownews.kcore.de/ns/1.0/\"", urlfile);
|
|
|
|
fputs (
|
|
">\n"
|
|
" <head>\n"
|
|
" <title>" SNOWNEWS_NAME " subscriptions</title>\n"
|
|
" </head>\n"
|
|
" <body>\n"
|
|
, urlfile);
|
|
|
|
// Write one outline element per feed
|
|
for (const struct feed* f = _feed_list; f; f = f->next) {
|
|
fputs ("\t<outline", urlfile);
|
|
char* title = NULL;
|
|
if (f->custom_title)
|
|
title = f->custom_title;
|
|
else if (f->title)
|
|
title = f->title;
|
|
if (title) {
|
|
char* encoded_title = (char*) xmlEncodeSpecialChars (NULL, (xmlChar*) title);
|
|
fprintf (urlfile, " text=\"%s\"", encoded_title);
|
|
free (encoded_title);
|
|
}
|
|
if (f->feedurl) {
|
|
char* encoded_feedurl = (char*) xmlEncodeSpecialChars (NULL, (xmlChar*) f->feedurl);
|
|
fprintf (urlfile, " xmlUrl=\"%s\"", encoded_feedurl);
|
|
free (encoded_feedurl);
|
|
}
|
|
if (f->feedcategories) {
|
|
fputs (" category=\"", urlfile);
|
|
for (const struct feedcategories* c = f->feedcategories; c; c = c->next) {
|
|
fputs (c->name, urlfile);
|
|
if (c->next)
|
|
fputc (',', urlfile);
|
|
}
|
|
fputs ("\"", urlfile);
|
|
}
|
|
if (f->perfeedfilter)
|
|
fprintf (urlfile, " snow:filter=\"%s\"", f->perfeedfilter);
|
|
fputs ("/>\n", urlfile);
|
|
}
|
|
fputs (
|
|
" </body>\n"
|
|
"</opml>\n", urlfile);
|
|
fclose (urlfile);
|
|
_feed_list_changed = false;
|
|
}
|
|
|
|
static void WriteFeedCache (const struct feed* feed)
|
|
{
|
|
char* hashme = Hashify (feed->feedurl);
|
|
char cachefilename [PATH_MAX];
|
|
CacheFilePath (hashme, cachefilename, sizeof(cachefilename));
|
|
free (hashme);
|
|
|
|
// Check if the feed has been modified since last loaded from this file
|
|
struct stat cachest;
|
|
if (0 == stat (cachefilename, &cachest) && cachest.st_mtime >= feed->mtime)
|
|
return;
|
|
|
|
FILE* cache = fopen (cachefilename, "w");
|
|
if (!cache) {
|
|
syslog (LOG_ERR, "error writing cache file '%s': %s", cachefilename, strerror (errno));
|
|
return;
|
|
}
|
|
|
|
fputs (
|
|
"<?xml version=\"1.0\" ?>\n\n"
|
|
"<rdf:RDF\n"
|
|
"\txmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"
|
|
"\txmlns=\"http://purl.org/rss/1.0/\"\n"
|
|
"\txmlns:snow=\"http://snownews.kcore.de/ns/1.0/\">\n\n", cache);
|
|
|
|
if (feed->lastmodified)
|
|
fprintf (cache, "<snow:lastmodified>%ld</snow:lastmodified>\n", feed->lastmodified);
|
|
|
|
char* encoded = (char*) xmlEncodeEntitiesReentrant (NULL, (xmlChar*) feed->feedurl);
|
|
fprintf (cache, "<channel rdf:about=\"%s\">\n<title>", encoded);
|
|
free (encoded);
|
|
|
|
if (feed->original)
|
|
fprintf (cache, "<![CDATA[%s]]>", feed->original);
|
|
else if (feed->title)
|
|
fprintf (cache, "<![CDATA[%s]]>", feed->title);
|
|
fputs ("</title>\n<link>", cache);
|
|
if (feed->link) {
|
|
encoded = (char*) xmlEncodeEntitiesReentrant (NULL, (xmlChar*) feed->link);
|
|
fputs (encoded, cache);
|
|
free (encoded);
|
|
}
|
|
fputs ("</link>\n<description>", cache);
|
|
if (feed->description)
|
|
fprintf (cache, "<![CDATA[%s]]>", feed->description);
|
|
fputs ("</description>\n</channel>\n\n", cache);
|
|
|
|
for (const struct newsitem * item = feed->items; item; item = item->next) {
|
|
fputs ("<item rdf:about=\"", cache);
|
|
if (item->data->link) {
|
|
encoded = (char*) xmlEncodeEntitiesReentrant (NULL, (xmlChar*) item->data->link);
|
|
fputs (encoded, cache);
|
|
free (encoded);
|
|
}
|
|
fputs ("\">\n<title>", cache);
|
|
if (item->data->title)
|
|
fprintf (cache, "<![CDATA[%s]]>", item->data->title);
|
|
fputs ("</title>\n<link>", cache);
|
|
if (item->data->link) {
|
|
encoded = (char*) xmlEncodeEntitiesReentrant (NULL, (xmlChar*) item->data->link);
|
|
fputs (encoded, cache);
|
|
free (encoded);
|
|
}
|
|
fputs ("</link>\n<description>", cache);
|
|
if (item->data->description)
|
|
fprintf (cache, "<![CDATA[%s]]>", item->data->description);
|
|
fputs ("</description>\n<snow:readstatus>", cache);
|
|
putc ('0' + item->data->readstatus, cache);
|
|
fputs ("</snow:readstatus>\n<snow:hash>", cache);
|
|
if (item->data->hash)
|
|
fputs (item->data->hash, cache);
|
|
fputs ("</snow:hash>\n", cache);
|
|
fprintf (cache, "<snow:date>%u</snow:date>\n", item->data->date);
|
|
fputs ("</item>\n\n", cache);
|
|
}
|
|
fputs ("</rdf:RDF>", cache);
|
|
fclose (cache);
|
|
}
|
|
|
|
// Write in memory structures to disk cache.
|
|
// Usually called before program exit.
|
|
void WriteCache (void)
|
|
{
|
|
UIStatus (_("Saving settings ["), 0, 0);
|
|
|
|
WriteFeedUrls();
|
|
|
|
// Save feed cache
|
|
unsigned numfeeds = 0;
|
|
for (const struct feed* f = _feed_list; f; f = f->next)
|
|
++numfeeds;
|
|
|
|
unsigned count = 1;
|
|
int oldnumobjects = 0;
|
|
unsigned titlestrlen = utf8_length (_("Saving settings ["));
|
|
|
|
for (const struct feed* cur_ptr = _feed_list; cur_ptr; cur_ptr = cur_ptr->next) {
|
|
// Progress bar
|
|
int numobjects = count * (COLS - titlestrlen - 2) / numfeeds - 2;
|
|
if (numobjects < 1)
|
|
numobjects = 1;
|
|
if (numobjects > oldnumobjects) {
|
|
DrawProgressBar (numobjects, titlestrlen);
|
|
oldnumobjects = numobjects;
|
|
}
|
|
++count;
|
|
|
|
// Discard smart feeds from cache.
|
|
if (cur_ptr->smartfeed)
|
|
continue;
|
|
|
|
// Write cache.
|
|
WriteFeedCache (cur_ptr);
|
|
}
|
|
return;
|
|
}
|