// For license of this file, see /LICENSE.md. #include "services/standard/standardfeed.h" #include "database/databasequeries.h" #include "definitions/definitions.h" #include "exceptions/applicationexception.h" #include "exceptions/feedrecognizedbutfailedexception.h" #include "exceptions/networkexception.h" #include "exceptions/scriptexception.h" #include "miscellaneous/settings.h" #include "miscellaneous/textfactory.h" #include "services/standard/gui/formstandardfeeddetails.h" #include "services/standard/standardserviceroot.h" #if defined(NO_LITE) #include "gui/webviewers/webengine/webengineviewer.h" #include "network-web/webengine/webenginepage.h" #endif #include "services/standard/parsers/atomparser.h" #include "services/standard/parsers/jsonparser.h" #include "services/standard/parsers/rdfparser.h" #include "services/standard/parsers/rssparser.h" #include "services/standard/parsers/sitemapparser.h" #if defined(ENABLE_COMPRESSED_SITEMAP) #include "3rd-party/qcompressor/qcompressor.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include StandardFeed::StandardFeed(RootItem* parent_item) : Feed(parent_item) { m_type = Type::Rss0X; m_sourceType = SourceType::Url; m_encoding = m_postProcessScript = QString(); m_protection = NetworkFactory::NetworkAuthentication::NoAuthentication; m_username = QString(); m_password = QString(); } StandardFeed::StandardFeed(const StandardFeed& other) : Feed(other) { m_type = other.type(); m_postProcessScript = other.postProcessScript(); m_sourceType = other.sourceType(); m_encoding = other.encoding(); m_protection = other.protection(); m_username = other.username(); m_password = other.password(); } QList StandardFeed::contextMenuFeedsList() { return serviceRoot()->getContextMenuForFeed(this); } QString StandardFeed::additionalTooltip() const { QString stat = getStatusDescription(); QString stat_string = statusString(); if (!stat_string.simplified().isEmpty()) { stat += QSL(" (%1)").arg(stat_string); } auto filters = messageFilters(); auto std_fltrs = boolinq::from(filters) .select([](const QPointer& pn) { return pn->name(); }) .toStdList(); QStringList fltrs = FROM_STD_LIST(QStringList, std_fltrs); QString base_tooltip = tr("Auto-update status: %1\n" "Active message filters: %2\n" "Status: %3\n" "Source: %4\n" "Item ID: %5\n") .arg(getAutoUpdateStatusDescription(), filters.size() > 0 ? QSL("%1 (%2)").arg(QString::number(filters.size()), fltrs.join(QSL(", "))) : QString::number(filters.size()), stat, m_sourceType == SourceType::Url ? QString("%1").arg(source().left(100)) : source().left(100), customId()); return base_tooltip + tr("Encoding: %1\n" "Type: %2\n" "Post-processing script: %3") .arg(encoding(), StandardFeed::typeToString(type()), m_postProcessScript.isEmpty() ? QSL("-") : m_postProcessScript); } bool StandardFeed::canBeDeleted() const { return true; } StandardServiceRoot* StandardFeed::serviceRoot() const { return qobject_cast(getParentServiceRoot()); } bool StandardFeed::deleteItem() { if (removeItself()) { serviceRoot()->requestItemRemoval(this); return true; } else { return false; } } NetworkFactory::NetworkAuthentication StandardFeed::protection() const { return m_protection; } void StandardFeed::setProtection(NetworkFactory::NetworkAuthentication protect) { m_protection = protect; } QString StandardFeed::username() const { return m_username; } void StandardFeed::setUsername(const QString& username) { m_username = username; } QString StandardFeed::password() const { return m_password; } void StandardFeed::setPassword(const QString& password) { m_password = password; } QVariantHash StandardFeed::customDatabaseData() const { QVariantHash data; data[QSL("source_type")] = int(sourceType()); data[QSL("type")] = int(type()); data[QSL("encoding")] = encoding(); data[QSL("post_process")] = postProcessScript(); data[QSL("protected")] = int(protection()); data[QSL("username")] = username(); data[QSL("password")] = TextFactory::encrypt(password()); return data; } void StandardFeed::setCustomDatabaseData(const QVariantHash& data) { setSourceType(SourceType(data[QSL("source_type")].toInt())); setType(Type(data[QSL("type")].toInt())); setEncoding(data[QSL("encoding")].toString()); setPostProcessScript(data[QSL("post_process")].toString()); setProtection(NetworkFactory::NetworkAuthentication(data[QSL("protected")].toInt())); setUsername(data[QSL("username")].toString()); setPassword(TextFactory::decrypt(data[QSL("password")].toString())); } QString StandardFeed::typeToString(StandardFeed::Type type) { switch (type) { case Type::Atom10: return QSL("ATOM 1.0"); case Type::Rdf: return QSL("RDF (RSS 1.0)"); case Type::Rss0X: return QSL("RSS 0.91/0.92/0.93"); case Type::Json: return QSL("JSON 1.0/1.1"); case Type::Sitemap: return QSL("Sitemap"); case Type::Rss2X: default: return QSL("RSS 2.0/2.0.1"); } } QString StandardFeed::sourceTypeToString(StandardFeed::SourceType type) { switch (type) { case StandardFeed::SourceType::Url: return QSL("URL"); case StandardFeed::SourceType::Script: return tr("Script"); case StandardFeed::SourceType::LocalFile: return tr("Local file"); case StandardFeed::SourceType::EmbeddedBrowser: return tr("Built-in web browser"); default: return tr("Unknown"); } } void StandardFeed::fetchMetadataForItself() { try { StandardFeed* metadata = guessFeed(sourceType(), source(), postProcessScript(), protection(), true, username(), password(), getParentServiceRoot()->networkProxy()); // Copy metadata to our object. setTitle(metadata->title()); setDescription(metadata->description()); setType(metadata->type()); setEncoding(metadata->encoding()); setIcon(metadata->icon()); metadata->deleteLater(); QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); DatabaseQueries::createOverwriteFeed(database, this, getParentServiceRoot()->accountId(), parent()->id()); serviceRoot()->itemChanged({this}); } catch (const ApplicationException& ex) { qCriticalNN << LOGSEC_DB << "Cannot overwrite feed:" << QUOTE_W_SPACE_DOT(ex.message()); qApp->showGuiMessage(Notification::Event::GeneralEvent, {tr("Cannot save feed data"), tr("Cannot save data for feed: %1").arg(ex.message()), QSystemTrayIcon::MessageIcon::Critical}); } } QString StandardFeed::postProcessScript() const { return m_postProcessScript; } void StandardFeed::setPostProcessScript(const QString& post_process_script) { m_postProcessScript = post_process_script; } StandardFeed::SourceType StandardFeed::sourceType() const { return m_sourceType; } void StandardFeed::setSourceType(SourceType source_type) { m_sourceType = source_type; } StandardFeed* StandardFeed::guessFeed(StandardFeed::SourceType source_type, const QString& source, const QString& post_process_script, NetworkFactory::NetworkFactory::NetworkAuthentication protection, bool fetch_icons, const QString& username, const QString& password, const QNetworkProxy& custom_proxy) { auto timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt(); QByteArray feed_contents; QString content_type; if (source_type == StandardFeed::SourceType::Url) { QList> headers = { NetworkFactory::generateBasicAuthHeader(protection, username, password)}; NetworkResult network_result = NetworkFactory::performNetworkOperation(source, timeout, QByteArray(), feed_contents, QNetworkAccessManager::Operation::GetOperation, headers, false, {}, {}, custom_proxy); content_type = network_result.m_contentType; if (network_result.m_networkError != QNetworkReply::NetworkError::NoError) { throw NetworkException(network_result.m_networkError); } } else if (source_type == StandardFeed::SourceType::EmbeddedBrowser) { #if defined(NO_LITE) WebEnginePage page; WebEngineViewer viewer; // NOTE: Viewer must be present or JavaScript just does not run. viewer.setPage(&page); viewer.setAttribute(Qt::WA_DontShowOnScreen); viewer.show(); feed_contents = page.pageHtml(source).toUtf8(); // IOFactory::writeFile("a.html", feed_contents); #else throw ApplicationException(tr("this source type cannot be used on 'lite' %1 build").arg(QSL(APP_NAME))); #endif } else if (source_type == StandardFeed::SourceType::LocalFile) { feed_contents = IOFactory::readFile(source); } else { qDebugNN << LOGSEC_CORE << "Running custom script for guessing" << QUOTE_W_SPACE(source) << "to obtain feed data."; // Use script to generate feed file. feed_contents = generateFeedFileWithScript(source, timeout); } // Sitemap parser supports gzip-encoded data too. // We need to decode it here before encoding // stuff kicks in. if (SitemapParser::isGzip(feed_contents)) { #if defined(ENABLE_COMPRESSED_SITEMAP) qWarningNN << LOGSEC_CORE << "Decompressing gzipped feed data."; QByteArray uncompressed_feed_contents; if (!QCompressor::gzipDecompress(feed_contents, uncompressed_feed_contents)) { throw ApplicationException("gzip decompression failed"); } feed_contents = uncompressed_feed_contents; #else qWarningNN << LOGSEC_CORE << "This feed is gzipped."; #endif } if (!post_process_script.simplified().isEmpty()) { qDebugNN << LOGSEC_CORE << "Post-processing obtained feed data with custom script for guessing" << QUOTE_W_SPACE_DOT(post_process_script); feed_contents = postProcessFeedFileWithScript(post_process_script, feed_contents, timeout); } StandardFeed* feed = nullptr; QList icon_possible_locations; QList> parsers; parsers.append(QSharedPointer(new AtomParser({}))); parsers.append(QSharedPointer(new RssParser({}))); parsers.append(QSharedPointer(new RdfParser({}))); parsers.append(QSharedPointer(new JsonParser({}))); parsers.append(QSharedPointer(new SitemapParser({}))); for (const QSharedPointer& parser : parsers) { try { QPair> res = parser->guessFeed(feed_contents, content_type); feed = res.first; icon_possible_locations = res.second; break; } catch (const FeedRecognizedButFailedException& format_ex) { // Parser reports that it is right parser for this feed // but its support is not enabled or available or it is broken. // In this case abort. throw format_ex; } catch (const ApplicationException& ex) { qWarningNN << LOGSEC_CORE << "Feed guessing error:" << QUOTE_W_SPACE_DOT(ex.message()); } } if (feed == nullptr) { throw ApplicationException(tr("feed format not recognized")); } if (source_type == SourceType::Url && icon_possible_locations.isEmpty()) { // We have no source for feed icon, we use the URL of the feed file itself. icon_possible_locations.append({source, false}); } if (fetch_icons) { // Try to obtain icon. QPixmap icon_data; if (NetworkFactory::downloadIcon(icon_possible_locations, DOWNLOAD_TIMEOUT, icon_data, {}, custom_proxy) == QNetworkReply::NetworkError::NoError) { // Icon for feed was downloaded and is stored now in icon_data. feed->setIcon(icon_data); } } return feed; } Qt::ItemFlags StandardFeed::additionalFlags() const { return Feed::additionalFlags() | Qt::ItemFlag::ItemIsDragEnabled; } bool StandardFeed::performDragDropChange(RootItem* target_item) { QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); try { DatabaseQueries::createOverwriteFeed(database, this, getParentServiceRoot()->accountId(), target_item->id()); serviceRoot()->requestItemReassignment(this, target_item); return true; } catch (const ApplicationException& ex) { qCriticalNN << LOGSEC_DB << "Cannot overwrite feed:" << QUOTE_W_SPACE_DOT(ex.message()); qApp->showGuiMessage(Notification::Event::GeneralEvent, {tr("Cannot move feed"), tr("Cannot move feed, detailed " "information was logged via " "debug log."), QSystemTrayIcon::MessageIcon::Critical}); return false; } } bool StandardFeed::removeItself() { QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className()); return DatabaseQueries::deleteFeed(database, this, getParentServiceRoot()->accountId()); } QString StandardFeed::lastEtag() const { return m_lastEtag; } void StandardFeed::setLastEtag(const QString& etag) { m_lastEtag = etag; } StandardFeed::Type StandardFeed::type() const { return m_type; } void StandardFeed::setType(StandardFeed::Type type) { m_type = type; } QString StandardFeed::encoding() const { return m_encoding; } void StandardFeed::setEncoding(const QString& encoding) { m_encoding = encoding; } QStringList StandardFeed::prepareExecutionLine(const QString& execution_line) { auto args = TextFactory::tokenizeProcessArguments(execution_line); return qApp->replaceUserDataFolderPlaceholder(args); } QByteArray StandardFeed::runScriptProcess(const QStringList& cmd_args, const QString& working_directory, int run_timeout, bool provide_input, const QString& input) { QProcess process; if (provide_input) { process.setInputChannelMode(QProcess::InputChannelMode::ManagedInputChannel); } process.setProcessEnvironment(QProcessEnvironment::systemEnvironment()); process.setProcessChannelMode(QProcess::ProcessChannelMode::SeparateChannels); process.setWorkingDirectory(working_directory); process.setProgram(cmd_args.at(0)); if (cmd_args.size() > 1) { process.setArguments(cmd_args.mid(1)); } if (!process.open()) { switch (process.error()) { case QProcess::ProcessError::FailedToStart: throw ScriptException(ScriptException::Reason::InterpreterNotFound); default: break; } } if (provide_input) { process.write(input.toUtf8()); process.closeWriteChannel(); } if (process.waitForFinished(run_timeout) && process.exitStatus() == QProcess::ExitStatus::NormalExit && process.exitCode() == EXIT_SUCCESS) { auto raw_output = process.readAllStandardOutput(); auto raw_error = process.readAllStandardError(); if (!raw_error.simplified().isEmpty()) { qWarningNN << LOGSEC_CORE << "Received error output from " "custom script even if it " "reported that it exited " "normally:" << QUOTE_W_SPACE_DOT(raw_error); } return raw_output; } else { auto raw_error = process.readAllStandardError().simplified(); if (raw_error.isEmpty()) { raw_error = process.readAllStandardOutput().simplified(); } switch (process.error()) { case QProcess::ProcessError::Timedout: throw ScriptException(ScriptException::Reason::InterpreterTimeout); default: throw ScriptException(ScriptException::Reason::InterpreterError, raw_error); } } } QByteArray StandardFeed::generateFeedFileWithScript(const QString& execution_line, int run_timeout) { auto prepared_query = prepareExecutionLine(execution_line); if (prepared_query.isEmpty()) { throw ScriptException(ScriptException::Reason::ExecutionLineInvalid); } return runScriptProcess(prepared_query, qApp->userDataFolder(), run_timeout, false); } QByteArray StandardFeed::postProcessFeedFileWithScript(const QString& execution_line, const QString& raw_feed_data, int run_timeout) { auto prepared_query = prepareExecutionLine(execution_line); if (prepared_query.isEmpty()) { throw ScriptException(ScriptException::Reason::ExecutionLineInvalid); } return runScriptProcess(prepared_query, qApp->userDataFolder(), run_timeout, true, raw_feed_data); }