// This file is part of RSS Guard. // // Copyright (C) 2011-2015 by Martin Rotter // // RSS Guard 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 3 of the License, or // (at your option) any later version. // // RSS Guard 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 RSS Guard. If not, see . #include "core/feedsmodelfeed.h" #include "definitions/definitions.h" #include "core/parsingfactory.h" #include "miscellaneous/databasefactory.h" #include "miscellaneous/textfactory.h" #include "miscellaneous/settings.h" #include "miscellaneous/iconfactory.h" #include "network-web/networkfactory.h" #include #include #include #include #include #include #include #include #include void FeedsModelFeed::init() { m_passwordProtected = false; m_username = QString(); m_password = QString(); m_status = Normal; m_networkError = QNetworkReply::NoError; m_type = Rss0X; m_totalCount = 0; m_unreadCount = 0; m_autoUpdateType = DontAutoUpdate; m_autoUpdateInitialInterval = DEFAULT_AUTO_UPDATE_INTERVAL; m_autoUpdateRemainingInterval = DEFAULT_AUTO_UPDATE_INTERVAL; m_encoding = QString(); m_url = QString(); m_kind = FeedsModelRootItem::Feed; } FeedsModelFeed::FeedsModelFeed(FeedsModelRootItem *parent_item) : FeedsModelRootItem(parent_item) { init(); } FeedsModelFeed::FeedsModelFeed(const FeedsModelFeed &other) : FeedsModelRootItem(NULL) { m_passwordProtected = other.passwordProtected(); m_username = other.username(); m_password = other.password(); m_status = other.status(); m_networkError = other.networkError(); m_type = other.type(); m_totalCount = other.countOfAllMessages(); m_unreadCount = other.countOfUnreadMessages(); m_autoUpdateType = other.autoUpdateType(); m_autoUpdateInitialInterval = other.autoUpdateInitialInterval(); m_autoUpdateRemainingInterval = other.autoUpdateRemainingInterval(); m_encoding = other.encoding(); m_url = other.url(); m_kind = FeedsModelRootItem::Feed; m_title = other.title(); m_id = other.id(); m_icon = other.icon(); m_childItems = other.childItems(); m_parentItem = other.parent(); m_creationDate = other.creationDate(); m_description = other.description(); } FeedsModelFeed::~FeedsModelFeed() { qDebug("Destroying FeedsModelFeed instance."); } int FeedsModelFeed::childCount() const { // Because feed has no children. return 0; } int FeedsModelFeed::countOfAllMessages() const { return m_totalCount; } int FeedsModelFeed::countOfUnreadMessages() const { return m_unreadCount; } QString FeedsModelFeed::typeToString(FeedsModelFeed::Type type) { switch (type) { case Atom10: return "ATOM 1.0"; case Rdf: return "RDF (RSS 1.0)"; case Rss0X: return "RSS 0.91/0.92/0.93"; case Rss2X: default: return "RSS 2.0/2.0.1"; } } void FeedsModelFeed::updateCounts(bool including_total_count, bool update_feed_statuses) { QSqlDatabase database = qApp->database()->connection("FeedsModelFeed", DatabaseFactory::FromSettings); QSqlQuery query_all(database); query_all.setForwardOnly(true); if (including_total_count) { if (query_all.exec(QString("SELECT count(*) FROM Messages WHERE feed = %1 AND is_deleted = 0;").arg(id())) && query_all.next()) { m_totalCount = query_all.value(0).toInt(); } } // Obtain count of unread messages. if (query_all.exec(QString("SELECT count(*) FROM Messages WHERE feed = %1 AND is_deleted = 0 AND is_read = 0;").arg(id())) && query_all.next()) { int new_unread_count = query_all.value(0).toInt(); if (update_feed_statuses && m_status == NewMessages && new_unread_count < m_unreadCount) { m_status = Normal; } m_unreadCount = new_unread_count; } } QPair FeedsModelFeed::guessFeed(const QString &url, const QString &username, const QString &password) { QPair result; result.first = NULL; QByteArray feed_contents; NetworkResult network_result = NetworkFactory::downloadFeedFile(url, qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(), feed_contents, !username.isEmpty(), username, password); result.second = network_result.first; if (result.second == QNetworkReply::NoError) { // Feed XML was obtained, now we need to try to guess // its encoding before we can read further data. QString xml_schema_encoding; QString xml_contents_encoded; QRegExp encoding_rexp("encoding=\"[^\"]\\S+\""); if (encoding_rexp.indexIn(feed_contents) != -1 && !(xml_schema_encoding = encoding_rexp.cap(0)).isEmpty()) { // Some "encoding" attribute was found get the encoding // out of it. encoding_rexp.setPattern("[^\"]\\S+[^\"]"); encoding_rexp.indexIn(xml_schema_encoding, 9); xml_schema_encoding = encoding_rexp.cap(0); } if (result.first == NULL) { result.first = new FeedsModelFeed(); } QTextCodec *custom_codec = QTextCodec::codecForName(xml_schema_encoding.toLocal8Bit()); if (custom_codec != NULL) { // Feed encoding was probably guessed. xml_contents_encoded = custom_codec->toUnicode(feed_contents); result.first->setEncoding(xml_schema_encoding); } else { // Feed encoding probably not guessed, set it as // default. xml_contents_encoded = feed_contents; result.first->setEncoding(DEFAULT_FEED_ENCODING); } // Feed XML was obtained, guess it now. QDomDocument xml_document; QString error_msg; int error_line, error_column; if (!xml_document.setContent(xml_contents_encoded, &error_msg, &error_line, &error_column)) { qDebug("XML of feed '%s' is not valid and cannot be loaded. Error: '%s' " "(line %d, column %d).", qPrintable(url), qPrintable(error_msg), error_line, error_column); result.second = QNetworkReply::UnknownContentError; // XML is invalid, exit. return result; } QDomElement root_element = xml_document.documentElement(); QString root_tag_name = root_element.tagName(); QList icon_possible_locations; icon_possible_locations.append(url); if (root_tag_name == "rdf:RDF") { // We found RDF feed. QDomElement channel_element = root_element.namedItem("channel").toElement(); result.first->setType(Rdf); result.first->setTitle(channel_element.namedItem("title").toElement().text()); result.first->setDescription(channel_element.namedItem("description").toElement().text()); QString source_link = channel_element.namedItem("link").toElement().text(); if (!source_link.isEmpty()) { icon_possible_locations.prepend(source_link); } } else if (root_tag_name == "rss") { // We found RSS 0.91/0.92/0.93/2.0/2.0.1 feed. QString rss_type = root_element.attribute("version", "2.0"); if (rss_type == "0.91" || rss_type == "0.92" || rss_type == "0.93") { result.first->setType(Rss0X); } else { result.first->setType(Rss2X); } QDomElement channel_element = root_element.namedItem("channel").toElement(); result.first->setTitle(channel_element.namedItem("title").toElement().text()); result.first->setDescription(channel_element.namedItem("description").toElement().text()); QString source_link = channel_element.namedItem("link").toElement().text(); if (!source_link.isEmpty()) { icon_possible_locations.prepend(source_link); } } else if (root_tag_name == "feed") { // We found ATOM feed. result.first->setType(Atom10); result.first->setTitle(root_element.namedItem("title").toElement().text()); result.first->setDescription(root_element.namedItem("subtitle").toElement().text()); QString source_link = root_element.namedItem("link").toElement().text(); if (!source_link.isEmpty()) { icon_possible_locations.prepend(source_link); } } else { // File was downloaded and it really was XML file // but feed format was NOT recognized. result.second = QNetworkReply::UnknownContentError; } // Try to obtain icon. QIcon icon_data; if ((result.second = NetworkFactory::downloadIcon(icon_possible_locations, DOWNLOAD_TIMEOUT, icon_data)) == QNetworkReply::NoError) { // Icon for feed was downloaded and is stored now in _icon_data. result.first->setIcon(icon_data); } } return result; } QVariant FeedsModelFeed::data(int column, int role) const { switch (role) { case Qt::DisplayRole: if (column == FDS_MODEL_TITLE_INDEX) { return m_title; } else if (column == FDS_MODEL_COUNTS_INDEX) { return qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::CountFormat)).toString() .replace(PLACEHOLDER_UNREAD_COUNTS, QString::number(countOfUnreadMessages())) .replace(PLACEHOLDER_ALL_COUNTS, QString::number(countOfAllMessages())); } else { return QVariant(); } case Qt::EditRole: if (column == FDS_MODEL_TITLE_INDEX) { return m_title; } else if (column == FDS_MODEL_COUNTS_INDEX) { return countOfUnreadMessages(); } else { return QVariant(); } case Qt::DecorationRole: if (column == FDS_MODEL_TITLE_INDEX) { return m_icon; } else { return QVariant(); } case Qt::ToolTipRole: if (column == FDS_MODEL_TITLE_INDEX) { QString auto_update_string; switch (m_autoUpdateType) { case DontAutoUpdate: //: Describes feed auto-update status. auto_update_string = tr("does not use auto-update"); break; case DefaultAutoUpdate: //: Describes feed auto-update status. auto_update_string = tr("uses global settings"); break; case SpecificAutoUpdate: default: //: Describes feed auto-update status. auto_update_string = tr("uses specific settings " "(%n minute(s) to next auto-update)", 0, m_autoUpdateRemainingInterval); break; } //: Tooltip for feed. return tr("%1 (%2)" "%3\n\n" "Network status: %6\n" "Encoding: %4\n" "Auto-update status: %5").arg(m_title, FeedsModelFeed::typeToString(m_type), m_description.isEmpty() ? QString() : QString('\n') + m_description, m_encoding, auto_update_string, NetworkFactory::networkErrorText(m_networkError)); } else if (column == FDS_MODEL_COUNTS_INDEX) { //: Tooltip for "unread" column of feed list. return tr("%n unread message(s).", 0, countOfUnreadMessages()); } else { return QVariant(); } case Qt::TextAlignmentRole: if (column == FDS_MODEL_COUNTS_INDEX) { return Qt::AlignCenter; } else { return QVariant(); } case Qt::FontRole: return countOfUnreadMessages() > 0 ? m_boldFont : m_normalFont; case Qt::ForegroundRole: switch (m_status) { case NewMessages: return QColor(Qt::blue); case NetworkError: return QColor(Qt::red); default: return QVariant(); } default: return QVariant(); } } void FeedsModelFeed::update() { QByteArray feed_contents; int download_timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); m_networkError = NetworkFactory::downloadFeedFile(url(), download_timeout, feed_contents, passwordProtected(), username(), password()).first; if (m_networkError != QNetworkReply::NoError) { qWarning("Error during fetching of new messages for feed '%s' (id %d).", qPrintable(url()), id()); m_status = NetworkError; return; } else { m_status = Normal; } // Encode downloaded data for further parsing. QTextCodec *codec = QTextCodec::codecForName(encoding().toLocal8Bit()); QString formatted_feed_contents; if (codec == NULL) { // No suitable codec for this encoding was found. // Use non-converted data. formatted_feed_contents = feed_contents; } else { formatted_feed_contents = codec->toUnicode(feed_contents); } // Feed data are downloaded and encoded. // Parse data and obtain messages. QList messages; switch (type()) { case FeedsModelFeed::Rss0X: case FeedsModelFeed::Rss2X: messages = ParsingFactory::parseAsRSS20(formatted_feed_contents); break; case FeedsModelFeed::Rdf: messages = ParsingFactory::parseAsRDF(formatted_feed_contents); break; case FeedsModelFeed::Atom10: messages = ParsingFactory::parseAsATOM10(formatted_feed_contents); default: break; } updateMessages(messages); } bool FeedsModelFeed::removeItself() { QSqlDatabase database = qApp->database()->connection("FeedsModelFeed", DatabaseFactory::FromSettings); QSqlQuery query_remove(database); query_remove.setForwardOnly(true); // Remove all messages from this standard feed. query_remove.prepare("DELETE FROM Messages WHERE feed = :feed;"); query_remove.bindValue(":feed", id()); if (!query_remove.exec()) { return false; } // Remove feed itself. query_remove.prepare("DELETE FROM Feeds WHERE id = :feed;"); query_remove.bindValue(":feed", id()); return query_remove.exec(); } void FeedsModelFeed::updateMessages(const QList &messages) { int feed_id = id(); QSqlDatabase database = qApp->database()->connection("FeedsModelFeed", DatabaseFactory::FromSettings); bool remove_duplicates = qApp->settings()->value(GROUP(Messages), SETTING(Messages::RemoveDuplicates)).toBool(); // Prepare queries. QSqlQuery query_select(database); QSqlQuery query_update(database); QSqlQuery query_insert(database); // Used to check if given feed contains any message with given title, url and date_created. // WARNING: One feed CANNOT contain two (or more) messages with same AUTHOR AND TITLE AND URL AND DATE_CREATED. query_select.setForwardOnly(true); query_select.prepare("SELECT id, feed, date_created FROM Messages " "WHERE feed = :feed AND title = :title AND url = :url AND author = :author;"); // Used to insert new messages. query_insert.setForwardOnly(true); query_insert.prepare("INSERT INTO Messages " "(feed, title, url, author, date_created, contents, enclosures) " "VALUES (:feed, :title, :url, :author, :date_created, :contents, :enclosures);"); if (remove_duplicates) { query_update.setForwardOnly(true); query_update.prepare("UPDATE Messages SET contents = :contents enclosures = :enclosures WHERE id = :id;"); } if (!database.transaction()) { database.rollback(); qDebug("Transaction start for message downloader failed."); return; } foreach (Message message, messages) { // Check if messages contain relative URLs and if they do, then replace them. if (message.m_url.startsWith('/')) { QString new_message_url = url(); int last_slash = new_message_url.lastIndexOf('/'); if (last_slash >= 0) { new_message_url = new_message_url.left(last_slash); } new_message_url += message.m_url; message.m_url = new_message_url; } query_select.bindValue(":feed", feed_id); query_select.bindValue(":title", message.m_title); query_select.bindValue(":url", message.m_url); query_select.bindValue(":author", message.m_author); query_select.exec(); QList datetime_stamps; QList ids; while (query_select.next()) { ids << query_select.value(0).toInt(); datetime_stamps << query_select.value(2).value(); } query_select.finish(); if (datetime_stamps.isEmpty()) { // Message is not fetched in this feed yet. query_insert.bindValue(":feed", feed_id); query_insert.bindValue(":title", message.m_title); query_insert.bindValue(":url", message.m_url); query_insert.bindValue(":author", message.m_author); query_insert.bindValue(":date_created", message.m_created.toMSecsSinceEpoch()); query_insert.bindValue(":contents", message.m_contents); query_insert.bindValue(":enclosures", Enclosures::encodeEnclosuresToString(message.m_enclosures)); if (query_insert.exec() && query_insert.numRowsAffected() == 1) { setStatus(NewMessages); } query_insert.finish(); qDebug("Adding new message '%s' to DB.", qPrintable(message.m_title)); } else if (message.m_createdFromFeed && !datetime_stamps.contains(message.m_created.toMSecsSinceEpoch())) { if (remove_duplicates && datetime_stamps.size() == 1) { // Message is already in feed and new message has new unique time but user wishes to update existing // messages and there is exactly ONE existing duplicate. query_update.bindValue(":id", ids.at(0)); query_update.bindValue(":contents", message.m_contents); query_update.bindValue(":enclosures", Enclosures::encodeEnclosuresToString(message.m_enclosures)); query_update.exec(); query_update.finish(); qDebug("Updating contents of duplicate message '%s'.", qPrintable(message.m_title)); } else { // Message with same title, author and url exists, but new message has new unique time and // user does not wish to update duplicates. query_insert.bindValue(":feed", feed_id); query_insert.bindValue(":title", message.m_title); query_insert.bindValue(":url", message.m_url); query_insert.bindValue(":author", message.m_author); query_insert.bindValue(":date_created", message.m_created.toMSecsSinceEpoch()); query_insert.bindValue(":contents", message.m_contents); query_insert.bindValue(":enclosures", Enclosures::encodeEnclosuresToString(message.m_enclosures)); if (query_insert.exec() && query_insert.numRowsAffected() == 1) { setStatus(NewMessages); } query_insert.finish(); qDebug("Adding new duplicate (with potentially updated contents) message '%s' to DB.", qPrintable(message.m_title)); } } } if (!database.commit()) { database.rollback(); qDebug("Transaction commit for message downloader failed."); } } QNetworkReply::NetworkError FeedsModelFeed::networkError() const { return m_networkError; } FeedsModelFeed::FeedsModelFeed(const QSqlRecord &record) : FeedsModelRootItem(NULL) { m_kind = FeedsModelRootItem::Feed; setTitle(record.value(FDS_DB_TITLE_INDEX).toString()); setId(record.value(FDS_DB_ID_INDEX).toInt()); setDescription(record.value(FDS_DB_DESCRIPTION_INDEX).toString()); setCreationDate(TextFactory::parseDateTime(record.value(FDS_DB_DCREATED_INDEX).value()).toLocalTime()); setIcon(qApp->icons()->fromByteArray(record.value(FDS_DB_ICON_INDEX).toByteArray())); setEncoding(record.value(FDS_DB_ENCODING_INDEX).toString()); setUrl(record.value(FDS_DB_URL_INDEX).toString()); setPasswordProtected(record.value(FDS_DB_PROTECTED_INDEX).toBool()); setUsername(record.value(FDS_DB_USERNAME_INDEX).toString()); setPassword(record.value(FDS_DB_PASSWORD_INDEX).toString()); setAutoUpdateType(static_cast(record.value(FDS_DB_UPDATE_TYPE_INDEX).toInt())); setAutoUpdateInitialInterval(record.value(FDS_DB_UPDATE_INTERVAL_INDEX).toInt()); updateCounts(); }