You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdepim/akregator/src/article.cpp

480 lines
13 KiB

/*
This file is part of Akregator.
Copyright (C) 2004 Stanislav Karchebny <Stanislav.Karchebny@kdemail.net>
2005 Frank Osterfeld <frank.osterfeld at kdemail.net>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program 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 this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
As a special exception, permission is given to link this program
with any edition of TQt, and distribute the resulting executable,
without including the source code for TQt in the source distribution.
*/
#include "article.h"
#include "feed.h"
#include "feedstorage.h"
#include "storage.h"
#include "librss/librss.h"
#include "shared.h"
#include "utils.h"
#include <tqdatetime.h>
#include <tqdom.h>
#include <tqregexp.h>
#include <tqstringlist.h>
#include <tqvaluelist.h>
#include <krfcdate.h>
#include <kdebug.h>
#include <kurl.h>
namespace Akregator {
struct Article::Private : public Shared
{
/** The status of the article is stored in an int, the bits having the
following meaning:
0000 0001 Deleted
0000 0010 Trash
0000 0100 New
0000 1000 Read
0001 0000 Keep
*/
enum tqStatus {Deleted=0x01, Trash=0x02, New=0x04, Read=0x08, Keep=0x10};
TQString guid;
Backend::FeedStorage* archive;
Feed* feed;
// the variables below are initialized to null values in the Article constructor
// and then loaded on demand instead.
//
// to read their values, you should therefore use the accessor methods of the Article
// hash(), pubDate(), statusBits() rather than accessing them directly.
uint hash;
TQDateTime pubDate;
int status;
};
Article::Article() : d(new Private)
{
d->hash = 0;
d->status = 0;
d->feed = 0;
d->archive = 0;
}
Article::Article(const TQString& guid, Feed* feed) : d(new Private)
{
// this constructor should be as cheap as possible, so avoid calls to
// read information from the archive in here if possible
//
// d->hash, d->pubDate and d->status are loaded on-demand by
// the hash(), pubDate() and statusBits() methods respectively
d->feed = feed;
d->guid = guid;
d->archive = Backend::Storage::getInstance()->archiveFor(feed->xmlUrl());
d->status = 0;
}
void Article::initialize(RSS::Article article, Backend::FeedStorage* archive)
{
d->archive = archive;
d->status = Private::New;
d->hash = Utils::calcHash(article.title() + article.description() + article.author() + article.link().url()
+ article.commentsLink().url() );
d->guid = article.guid();
if (!d->archive->contains(d->guid))
{
d->archive->addEntry(d->guid);
if (article.meta("deleted") == "true")
{ // if article is in deleted state, we just add the status and omit the rest
d->status = Private::Read | Private::Deleted;
d->archive->setqStatus(d->guid, d->status);
}
else
{ // article is not deleted, let's add it to the archive
d->archive->setHash(d->guid, hash() );
TQString title = article.title().isEmpty() ? buildTitle(article.description()) : article.title();
d->archive->setTitle(d->guid, title);
d->archive->setDescription(d->guid, article.description());
d->archive->setLink(d->guid, article.link().url());
d->archive->setComments(d->guid, article.comments());
d->archive->setCommentsLink(d->guid, article.commentsLink().url());
d->archive->setGuidIsPermaLink(d->guid, article.guidIsPermaLink());
d->archive->setGuidIsHash(d->guid, article.meta("guidIsHash") == "true");
d->pubDate = article.pubDate().isValid() ? article.pubDate() : TQDateTime::tqcurrentDateTime();
d->archive->setPubDate(d->guid, d->pubDate.toTime_t());
d->archive->setAuthor(d->guid, article.author());
TQValueList<RSS::Category> cats = article.categories();
TQValueList<RSS::Category>::ConstIterator end = cats.end();
for (TQValueList<RSS::Category>::ConstIterator it = cats.begin(); it != end; ++it)
{
Backend::Category cat;
cat.term = (*it).category();
cat.scheme = (*it).domain();
cat.name = (*it).category();
d->archive->addCategory(d->guid, cat);
}
if (!article.enclosure().isNull())
{
d->archive->setEnclosure(d->guid, article.enclosure().url(), article.enclosure().type(), article.enclosure().length());
}
else
{
d->archive->removeEnclosure(d->guid);
}
TQString status = article.meta("status");
if (!status.isEmpty())
{
int statusInt = status.toInt();
if (statusInt == New)
statusInt = Unread;
setqStatus(statusInt);
}
setKeep(article.meta("keep") == "true");
}
}
else
{
// always update comments count, as it's not used for hash calculation
d->archive->setComments(d->guid, article.comments());
if ( hash() != d->archive->hash(d->guid)) //article is in archive, was it modified?
{ // if yes, update
d->pubDate.setTime_t(d->archive->pubDate(d->guid));
d->archive->setHash(d->guid, hash() );
TQString title = article.title().isEmpty() ? buildTitle(article.description()) : article.title();
d->archive->setTitle(d->guid, title);
d->archive->setDescription(d->guid, article.description());
d->archive->setLink(d->guid, article.link().url());
d->archive->setCommentsLink(d->guid, article.commentsLink().url());
d->archive->setAuthor(d->guid, article.author());
}
}
}
Article::Article(RSS::Article article, Feed* feed) : d(new Private)
{
//assert(feed)
d->feed = feed;
initialize(article, Backend::Storage::getInstance()->archiveFor(feed->xmlUrl()));
}
Article::Article(RSS::Article article, Backend::FeedStorage* archive) : d(new Private)
{
d->feed = 0;
initialize(article, archive);
}
bool Article::isNull() const
{
return d->archive == 0; // TODO: use proper null state
}
void Article::offsetPubDate(int secs)
{
d->pubDate = pubDate().addSecs(secs);
d->archive->setPubDate(d->guid, d->pubDate.toTime_t());
}
void Article::setDeleted()
{
if (isDeleted())
return;
setqStatus(Read);
d->status = Private::Deleted | Private::Read;
d->archive->setqStatus(d->guid, d->status);
d->archive->setDeleted(d->guid);
if (d->feed)
d->feed->setArticleDeleted(*this);
}
bool Article::isDeleted() const
{
return (statusBits() & Private::Deleted) != 0;
}
Article::Article(const Article &other) : d(new Private)
{
*this = other;
}
Article::~Article()
{
if (d->deref())
{
delete d;
d = 0;
}
}
Article &Article::operator=(const Article &other)
{
if (this != &other) {
other.d->ref();
if (d && d->deref())
delete d;
d = other.d;
}
return *this;
}
bool Article::operator<(const Article &other) const
{
return pubDate() > other.pubDate() ||
(pubDate() == other.pubDate() && guid() < other.guid() );
}
bool Article::operator<=(const Article &other) const
{
return (pubDate() > other.pubDate() || *this == other);
}
bool Article::operator>(const Article &other) const
{
return pubDate() < other.pubDate() ||
(pubDate() == other.pubDate() && guid() > other.guid() );
}
bool Article::operator>=(const Article &other) const
{
return (pubDate() > other.pubDate() || *this == other);
}
bool Article::operator==(const Article &other) const
{
return d->guid == other.guid();
}
int Article::statusBits() const
{
// delayed loading of status information from archive
if ( d->status == 0 )
{
d->status = d->archive->status(d->guid);
}
return d->status;
}
int Article::status() const
{
if ((statusBits() & Private::Read) != 0)
return Read;
if ((statusBits() & Private::New) != 0)
return New;
else
return Unread;
}
void Article::setqStatus(int stat)
{
// use status() rather than statusBits() here to filter out status flags that we are not
// interested in
int oldtqStatus = status();
if (oldtqStatus != stat)
{
switch (stat)
{
case Read:
d->status = ( d->status | Private::Read) & ~Private::New;
break;
case Unread:
d->status = ( d->status & ~Private::Read) & ~Private::New;
break;
case New:
d->status = ( d->status | Private::New) & ~Private::Read;
break;
}
d->archive->setqStatus(d->guid, d->status);
if (d->feed)
d->feed->setArticleChanged(*this, oldtqStatus);
}
}
TQString Article::title() const
{
return d->archive->title(d->guid);
}
TQString Article::author() const
{
return d->archive->author(d->guid);
}
KURL Article::link() const
{
return d->archive->link(d->guid);
}
TQString Article::description() const
{
return d->archive->description(d->guid);
}
TQString Article::guid() const
{
return d->guid;
}
KURL Article::commentsLink() const
{
return d->archive->commentsLink(d->guid);
}
int Article::comments() const
{
return d->archive->comments(d->guid);
}
bool Article::guidIsPermaLink() const
{
return d->archive->guidIsPermaLink(d->guid);
}
bool Article::guidIsHash() const
{
return d->archive->guidIsHash(d->guid);
}
uint Article::hash() const
{
// delayed loading of hash from archive
if ( d->hash == 0 )
{
d->hash = d->archive->hash(d->guid);
}
return d->hash;
}
bool Article::keep() const
{
return ( statusBits() & Private::Keep) != 0;
}
RSS::Enclosure Article::enclosure() const
{
bool hasEnc;
TQString url, type;
int length;
d->archive->enclosure(d->guid, hasEnc, url, type, length);
return hasEnc ? RSS::Enclosure(url, length, type) : RSS::Enclosure();
}
void Article::setKeep(bool keep)
{
d->status = keep ? ( statusBits() | Private::Keep) : ( statusBits() & ~Private::Keep);
d->archive->setqStatus(d->guid, d->status);
if (d->feed)
d->feed->setArticleChanged(*this);
}
void Article::addTag(const TQString& tag)
{
d->archive->addTag(d->guid, tag);
if (d->feed)
d->feed->setArticleChanged(*this);
}
void Article::removeTag(const TQString& tag)
{
d->archive->removeTag(d->guid, tag);
if (d->feed)
d->feed->setArticleChanged(*this);
}
bool Article::hasTag(const TQString& tag) const
{
return d->archive->tags(d->guid).contains(tag);
}
TQStringList Article::tags() const
{
return d->archive->tags(d->guid);
}
Feed* Article::feed() const
{ return d->feed; }
const TQDateTime& Article::pubDate() const
{
// delayed loading of publication date information from archive
if ( d->pubDate.isNull() )
{
d->pubDate.setTime_t(d->archive->pubDate(d->guid));
}
return d->pubDate;
}
TQString Article::buildTitle(const TQString& description)
{
TQString s = description;
if (description.stripWhiteSpace().isEmpty())
return "";
int i = s.find('>',500); /*avoid processing too much */
if (i != -1)
s = s.left(i+1);
TQRegExp rx("(<([^\\s>]*)(?:[^>]*)>)[^<]*", false);
TQString tagName, toReplace, replaceWith;
while (rx.search(s) != -1 )
{
tagName=rx.cap(2);
if (tagName=="SCRIPT"||tagName=="script")
toReplace=rx.cap(0); // strip tag AND tag contents
else if (tagName.startsWith("br") || tagName.startsWith("BR"))
{
toReplace=rx.cap(1);
replaceWith=" ";
}
else
toReplace=rx.cap(1); // strip just tag
s=s.replace(s.find(toReplace),toReplace.length(),replaceWith); // do the deed
}
if (s.length()> 90)
s=s.left(90)+"...";
return s.simplifyWhiteSpace();
}
} // namespace Akregator