diff --git a/src/definitions/definitions.h b/src/definitions/definitions.h index de6fe877c..d71f1dc08 100755 --- a/src/definitions/definitions.h +++ b/src/definitions/definitions.h @@ -88,6 +88,7 @@ #define INTERNAL_URL_BLANK "http://rssguard.blank" #define INTERNAL_URL_MESSAGE_HOST "rssguard.message" #define INTERNAL_URL_BLANK_HOST "rssguard.blank" +#define INTERNAL_URL_PASSATTACHMENT "http://rssguard.passattachment" #define FEED_INITIAL_OPML_PATTERN "feeds-%1.opml" diff --git a/src/gui/dialogs/formabout.cpp b/src/gui/dialogs/formabout.cpp index c0171e433..9cf8f7f07 100755 --- a/src/gui/dialogs/formabout.cpp +++ b/src/gui/dialogs/formabout.cpp @@ -41,28 +41,28 @@ void FormAbout::loadSettingsAndPaths() { void FormAbout::loadLicenseAndInformation() { try { - m_ui.m_txtLicenseGnu->setText(IOFactory::readTextFile(APP_INFO_PATH + QL1S("/COPYING_GNU_GPL_HTML"))); + m_ui.m_txtLicenseGnu->setText(IOFactory::readFile(APP_INFO_PATH + QL1S("/COPYING_GNU_GPL_HTML"))); } catch (...) { m_ui.m_txtLicenseGnu->setText(tr("License not found.")); } try { - m_ui.m_txtLicenseGnu->setText(IOFactory::readTextFile(APP_INFO_PATH + QL1S("/COPYING_GNU_GPL_HTML"))); + m_ui.m_txtLicenseGnu->setText(IOFactory::readFile(APP_INFO_PATH + QL1S("/COPYING_GNU_GPL_HTML"))); } catch (...) { m_ui.m_txtLicenseGnu->setText(tr("License not found.")); } try { - m_ui.m_txtChangelog->setText(IOFactory::readTextFile(APP_INFO_PATH + QL1S("/CHANGELOG"))); + m_ui.m_txtChangelog->setText(IOFactory::readFile(APP_INFO_PATH + QL1S("/CHANGELOG"))); } catch (...) { m_ui.m_txtChangelog->setText(tr("Changelog not found.")); } try { - m_ui.m_txtLicenseBsd->setText(IOFactory::readTextFile(APP_INFO_PATH + QL1S("/COPYING_BSD"))); + m_ui.m_txtLicenseBsd->setText(IOFactory::readFile(APP_INFO_PATH + QL1S("/COPYING_BSD"))); } catch (...) { m_ui.m_txtLicenseBsd->setText(tr("License not found.")); diff --git a/src/gui/webbrowser.cpp b/src/gui/webbrowser.cpp index 5974b764f..33fd32701 100755 --- a/src/gui/webbrowser.cpp +++ b/src/gui/webbrowser.cpp @@ -110,7 +110,7 @@ void WebBrowser::loadMessages(const QList& messages, RootItem* root) { m_root = root; if (!m_root.isNull()) { - m_webView->loadMessages(messages); + m_webView->loadMessages(messages, root); show(); } } diff --git a/src/gui/webviewer.cpp b/src/gui/webviewer.cpp index c3fb3bfbe..6e673fa2b 100755 --- a/src/gui/webviewer.cpp +++ b/src/gui/webviewer.cpp @@ -15,7 +15,7 @@ #include -WebViewer::WebViewer(QWidget* parent) : QWebEngineView(parent) { +WebViewer::WebViewer(QWidget* parent) : QWebEngineView(parent), m_root(nullptr) { WebPage* page = new WebPage(this); connect(page, &WebPage::messageStatusChangeRequested, this, &WebViewer::messageStatusChangeRequested); @@ -70,7 +70,7 @@ bool WebViewer::resetWebPageZoom() { } } -void WebViewer::loadMessages(const QList& messages) { +void WebViewer::loadMessages(const QList& messages, RootItem* root) { Skin skin = qApp->skins()->currentSkin(); QString messages_layout; QString single_message_layout = skin.m_layoutMarkup; @@ -80,7 +80,17 @@ void WebViewer::loadMessages(const QList& messages) { QString enclosure_images; foreach (const Enclosure& enclosure, message.m_enclosures) { - enclosures += skin.m_enclosureMarkup.arg(enclosure.m_url, tr("Attachment"), enclosure.m_mimeType); + QString enc_url; + + if (!enclosure.m_url.contains(QRegularExpression(QSL("^(http|ftp|\\/)")))) { + enc_url = QString(INTERNAL_URL_PASSATTACHMENT) + QL1S("/?") + enclosure.m_url; + } + else { + enc_url = enclosure.m_url; + } + + enclosures += skin.m_enclosureMarkup.arg(enc_url, + tr("Attachment"), enclosure.m_mimeType); if (enclosure.m_mimeType.startsWith(QSL("image/"))) { // Add thumbnail image. @@ -106,6 +116,7 @@ void WebViewer::loadMessages(const QList& messages) { .arg(enclosure_images)); } + m_root = root; m_messageContents = skin.m_layoutMarkupWrapper.arg(messages.size() == 1 ? messages.at(0).m_title : tr("Newspaper view"), messages_layout); bool previously_enabled = isEnabled(); @@ -115,10 +126,6 @@ void WebViewer::loadMessages(const QList& messages) { setEnabled(previously_enabled); } -void WebViewer::loadMessage(const Message& message) { - loadMessages(QList() << message); -} - void WebViewer::clear() { bool previously_enabled = isEnabled(); @@ -163,3 +170,7 @@ void WebViewer::wheelEvent(QWheelEvent* event) { } } } + +RootItem* WebViewer::root() const { + return m_root; +} diff --git a/src/gui/webviewer.h b/src/gui/webviewer.h index 2f8831f62..215708fc7 100755 --- a/src/gui/webviewer.h +++ b/src/gui/webviewer.h @@ -8,6 +8,8 @@ #include "core/message.h" #include "network-web/webpage.h" +class RootItem; + class WebViewer : public QWebEngineView { Q_OBJECT @@ -22,6 +24,7 @@ class WebViewer : public QWebEngineView { } WebPage* page() const; + RootItem* root() const; public slots: @@ -31,8 +34,7 @@ class WebViewer : public QWebEngineView { bool resetWebPageZoom(); void displayMessage(); - void loadMessages(const QList& messages); - void loadMessage(const Message& message); + void loadMessages(const QList& messages, RootItem* root); void clear(); protected: @@ -45,6 +47,7 @@ class WebViewer : public QWebEngineView { void messageStatusChangeRequested(int message_id, WebPage::MessageStatusChange change); private: + RootItem* m_root; QString m_messageContents; }; diff --git a/src/miscellaneous/iofactory.cpp b/src/miscellaneous/iofactory.cpp index f942032f7..cac400317 100755 --- a/src/miscellaneous/iofactory.cpp +++ b/src/miscellaneous/iofactory.cpp @@ -5,12 +5,12 @@ #include "definitions/definitions.h" #include "exceptions/ioexception.h" +#include #include #include #include #include #include -#include IOFactory::IOFactory() {} @@ -68,7 +68,7 @@ QString IOFactory::filterBadCharsFromFilename(const QString& name) { return value; } -QByteArray IOFactory::readTextFile(const QString& file_path) { +QByteArray IOFactory::readFile(const QString& file_path) { QFile input_file(file_path); QByteArray input_data; @@ -82,15 +82,11 @@ QByteArray IOFactory::readTextFile(const QString& file_path) { } } -void IOFactory::writeTextFile(const QString& file_path, const QByteArray& data, const QString& encoding) { - Q_UNUSED(encoding) +void IOFactory::writeFile(const QString& file_path, const QByteArray& data) { QFile input_file(file_path); - QTextStream stream(&input_file); - if (input_file.open(QIODevice::Text | QIODevice::WriteOnly)) { - stream << data; - stream.flush(); - input_file.flush(); + if (input_file.open(QIODevice::WriteOnly)) { + input_file.write(data); input_file.close(); } else { diff --git a/src/miscellaneous/iofactory.h b/src/miscellaneous/iofactory.h index 00b36c024..9fbb936ef 100755 --- a/src/miscellaneous/iofactory.h +++ b/src/miscellaneous/iofactory.h @@ -30,8 +30,8 @@ class IOFactory { // Returns contents of a file. // Throws exception when no such file exists. - static QByteArray readTextFile(const QString& file_path); - static void writeTextFile(const QString& file_path, const QByteArray& data, const QString& encoding = QSL("UTF-8")); + static QByteArray readFile(const QString& file_path); + static void writeFile(const QString& file_path, const QByteArray& data); // Copies file, overwrites destination. static bool copyFile(const QString& source, const QString& destination); diff --git a/src/miscellaneous/skinfactory.cpp b/src/miscellaneous/skinfactory.cpp index 9ffb69cb3..e563e453b 100755 --- a/src/miscellaneous/skinfactory.cpp +++ b/src/miscellaneous/skinfactory.cpp @@ -122,17 +122,17 @@ Skin SkinFactory::skinInfo(const QString& skin_name, bool* ok) const { // So if one uses "##/images/border.png" in QSS then it is // replaced by fully absolute path and target file can // be safely loaded. - skin.m_layoutMarkupWrapper = QString::fromUtf8(IOFactory::readTextFile(skin_folder + QL1S("html_wrapper.html"))); + skin.m_layoutMarkupWrapper = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_wrapper.html"))); skin.m_layoutMarkupWrapper = skin.m_layoutMarkupWrapper.replace(QSL("##"), APP_SKIN_PATH + QL1S("/") + skin_name); - skin.m_enclosureImageMarkup = QString::fromUtf8(IOFactory::readTextFile(skin_folder + QL1S("html_enclosure_image.html"))); + skin.m_enclosureImageMarkup = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_enclosure_image.html"))); skin.m_enclosureImageMarkup = skin.m_enclosureImageMarkup.replace(QSL("##"), APP_SKIN_PATH + QL1S("/") + skin_name); - skin.m_layoutMarkup = QString::fromUtf8(IOFactory::readTextFile(skin_folder + QL1S("html_single_message.html"))); + skin.m_layoutMarkup = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_single_message.html"))); skin.m_layoutMarkup = skin.m_layoutMarkup.replace(QSL("##"), APP_SKIN_PATH + QL1S("/") + skin_name); - skin.m_enclosureMarkup = QString::fromUtf8(IOFactory::readTextFile(skin_folder + QL1S("html_enclosure_every.html"))); + skin.m_enclosureMarkup = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_enclosure_every.html"))); skin.m_enclosureMarkup = skin.m_enclosureMarkup.replace(QSL("##"), APP_SKIN_PATH + QL1S("/") + skin_name); - skin.m_rawData = QString::fromUtf8(IOFactory::readTextFile(skin_folder + QL1S("theme.css"))); + skin.m_rawData = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("theme.css"))); skin.m_rawData = skin.m_rawData.replace(QSL("##"), APP_SKIN_PATH + QL1S("/") + skin_name); - skin.m_adblocked = QString::fromUtf8(IOFactory::readTextFile(skin_folder + QL1S("html_adblocked.html"))); + skin.m_adblocked = QString::fromUtf8(IOFactory::readFile(skin_folder + QL1S("html_adblocked.html"))); if (ok != nullptr) { *ok = !skin.m_author.isEmpty() && !skin.m_version.isEmpty() && diff --git a/src/miscellaneous/textfactory.cpp b/src/miscellaneous/textfactory.cpp index e0cc75b69..4ee5ccbbc 100755 --- a/src/miscellaneous/textfactory.cpp +++ b/src/miscellaneous/textfactory.cpp @@ -124,12 +124,12 @@ quint64 TextFactory::initializeSecretEncryptionKey() { QString encryption_file_path = qApp->settings()->pathName() + QDir::separator() + ENCRYPTION_FILE_NAME; try { - s_encryptionKey = (quint64) QString(IOFactory::readTextFile(encryption_file_path)).toLongLong(); + s_encryptionKey = (quint64) QString(IOFactory::readFile(encryption_file_path)).toLongLong(); } catch (ApplicationException) { // Well, key does not exist or is invalid, generate and save one. s_encryptionKey = generateSecretEncryptionKey(); - IOFactory::writeTextFile(encryption_file_path, QString::number(s_encryptionKey).toLocal8Bit()); + IOFactory::writeFile(encryption_file_path, QString::number(s_encryptionKey).toLocal8Bit()); } } diff --git a/src/network-web/adblock/adblocksubscription.cpp b/src/network-web/adblock/adblocksubscription.cpp index 277268f2c..045bf4542 100755 --- a/src/network-web/adblock/adblocksubscription.cpp +++ b/src/network-web/adblock/adblocksubscription.cpp @@ -281,7 +281,7 @@ void AdBlockCustomList::loadSubscription(const QStringList& disabledRules) { QString rules; try { - rules = QString::fromUtf8(IOFactory::readTextFile(filePath())); + rules = QString::fromUtf8(IOFactory::readFile(filePath())); } catch (ApplicationException&) {} diff --git a/src/network-web/webpage.cpp b/src/network-web/webpage.cpp index 8c3eb76d2..0c865b89f 100755 --- a/src/network-web/webpage.cpp +++ b/src/network-web/webpage.cpp @@ -4,9 +4,12 @@ #include "definitions/definitions.h" #include "gui/webviewer.h" +#include "services/abstract/rootitem.h" +#include "services/abstract/serviceroot.h" #include #include +#include WebPage::WebPage(QObject* parent) : QWebEnginePage(parent) { setBackgroundColor(Qt::transparent); @@ -45,6 +48,14 @@ void WebPage::javaScriptAlert(const QUrl& securityOrigin, const QString& msg) { } bool WebPage::acceptNavigationRequest(const QUrl& url, NavigationType type, bool isMainFrame) { + const RootItem* root = view()->root(); + + if (url.toString().startsWith(INTERNAL_URL_PASSATTACHMENT) && + root != nullptr && + root->getParentServiceRoot()->downloadAttachmentOnMyOwn(url)) { + return false; + } + if (url.host() == INTERNAL_URL_MESSAGE_HOST) { setHtml(view()->messageContents(), QUrl(INTERNAL_URL_MESSAGE)); return true; diff --git a/src/services/abstract/serviceroot.cpp b/src/services/abstract/serviceroot.cpp index 8783b455c..f78d17752 100755 --- a/src/services/abstract/serviceroot.cpp +++ b/src/services/abstract/serviceroot.cpp @@ -61,6 +61,11 @@ RecycleBin* ServiceRoot::recycleBin() const { return m_recycleBin; } +bool ServiceRoot::downloadAttachmentOnMyOwn(const QUrl& url) const { + Q_UNUSED(url) + return false; +} + QList ServiceRoot::contextMenu() { return serviceMenu(); } diff --git a/src/services/abstract/serviceroot.h b/src/services/abstract/serviceroot.h index 792a90f2f..d2aa65d35 100755 --- a/src/services/abstract/serviceroot.h +++ b/src/services/abstract/serviceroot.h @@ -33,6 +33,7 @@ class ServiceRoot : public RootItem { bool deleteViaGui(); bool markAsReadUnread(ReadStatus status); virtual RecycleBin* recycleBin() const; + virtual bool downloadAttachmentOnMyOwn(const QUrl& url) const; QList undeletedMessages() const; virtual bool supportsFeedAdding() const; diff --git a/src/services/gmail/definitions.h b/src/services/gmail/definitions.h index 3792c1869..76eff8b29 100755 --- a/src/services/gmail/definitions.h +++ b/src/services/gmail/definitions.h @@ -7,10 +7,13 @@ #define GMAIL_OAUTH_TOKEN_URL "https://accounts.google.com/o/oauth2/token" #define GMAIL_OAUTH_SCOPE "https://mail.google.com/" +#define GMAIL_API_GET_ATTACHMENT "https://www.googleapis.com/gmail/v1/users/me/messages/%20/attachments/" #define GMAIL_API_LABELS_LIST "https://www.googleapis.com/gmail/v1/users/me/labels" #define GMAIL_API_MSGS_LIST "https://www.googleapis.com/gmail/v1/users/me/messages" #define GMAIL_API_BATCH "https://www.googleapis.com/batch" +#define GMAIL_ATTACHMENT_SEP "####" + #define GMAIL_DEFAULT_BATCH_SIZE 50 #define GMAIL_MAX_BATCH_SIZE 999 #define GMAIL_MIN_BATCH_SIZE 20 diff --git a/src/services/gmail/gmailserviceroot.cpp b/src/services/gmail/gmailserviceroot.cpp index 68e99e75f..7d42940a8 100755 --- a/src/services/gmail/gmailserviceroot.cpp +++ b/src/services/gmail/gmailserviceroot.cpp @@ -10,8 +10,12 @@ #include "services/gmail/definitions.h" #include "services/gmail/gmailentrypoint.h" #include "services/gmail/gmailfeed.h" +#include "services/gmail/gui/formeditgmailaccount.h" #include "services/gmail/network/gmailnetworkfactory.h" +#include +#include + GmailServiceRoot::GmailServiceRoot(GmailNetworkFactory* network, RootItem* parent) : ServiceRoot(parent), CacheForServiceRoot(), m_serviceMenu(QList()), m_network(network) { if (network == nullptr) { @@ -100,14 +104,36 @@ void GmailServiceRoot::saveAccountDataToDatabase() { } } +bool GmailServiceRoot::downloadAttachmentOnMyOwn(const QUrl& url) const { + QString str_url = url.toString(); + QString attachment_id = str_url.mid(str_url.indexOf(QL1C('?')) + 1); + QStringList parts = attachment_id.split(QL1S(GMAIL_ATTACHMENT_SEP)); + Downloader* down = network()->downloadAttachment(parts.at(1)); + + connect(down, &Downloader::completed, [parts, down](QNetworkReply::NetworkError status, QByteArray contents) { + if (status == QNetworkReply::NetworkError::NoError) { + QString data = QJsonDocument::fromJson(contents).object()["data"].toString(); + + if (!data.isEmpty()) { + IOFactory::writeFile(parts.at(0), QByteArray::fromBase64(data.toLocal8Bit(), + QByteArray::Base64Option::Base64UrlEncoding)); + } + } + + down->deleteLater(); + }); + + return true; +} + bool GmailServiceRoot::canBeEdited() const { return true; } bool GmailServiceRoot::editViaGui() { - //FormEditInoreaderAccount form_pointer(qApp->mainFormWidget()); - // TODO: dodělat - //form_pointer.execForEdit(this); + FormEditGmailAccount form_pointer(qApp->mainFormWidget()); + + form_pointer.execForEdit(this); return true; } diff --git a/src/services/gmail/gmailserviceroot.h b/src/services/gmail/gmailserviceroot.h index 3dbed4d85..63078d09a 100755 --- a/src/services/gmail/gmailserviceroot.h +++ b/src/services/gmail/gmailserviceroot.h @@ -17,6 +17,8 @@ class GmailServiceRoot : public ServiceRoot, public CacheForServiceRoot { void saveAccountDataToDatabase(); + bool downloadAttachmentOnMyOwn(const QUrl& url) const; + void setNetwork(GmailNetworkFactory* network); GmailNetworkFactory* network() const; diff --git a/src/services/gmail/network/gmailnetworkfactory.cpp b/src/services/gmail/network/gmailnetworkfactory.cpp index 9e9deaa2a..40f85bba9 100755 --- a/src/services/gmail/network/gmailnetworkfactory.cpp +++ b/src/services/gmail/network/gmailnetworkfactory.cpp @@ -100,6 +100,22 @@ void GmailNetworkFactory::setUsername(const QString& username) { return decodeFeedCategoriesData(category_data); }*/ +Downloader* GmailNetworkFactory::downloadAttachment(const QString& attachment_id) { + Downloader* downloader = new Downloader(); + QString bearer = m_oauth2->bearer().toLocal8Bit(); + + if (bearer.isEmpty()) { + return nullptr; + } + + QString target_url = QString(GMAIL_API_GET_ATTACHMENT) + attachment_id; + + downloader->appendRawHeader(QString(HTTP_HEADERS_AUTHORIZATION).toLocal8Bit(), bearer.toLocal8Bit()); + downloader->downloadFile(target_url); + + return downloader; +} + QList GmailNetworkFactory::messages(const QString& stream_id, Feed::Status& error) { Downloader downloader; QEventLoop loop; @@ -402,9 +418,7 @@ bool GmailNetworkFactory::fillFullMessage(Message& msg, const QJsonObject& json, } else { // We have attachment. - // TODO: pokračovat tady, přidat způsob jak dát userovi možnost - // stahnout prilohy - msg.m_enclosures.append(Enclosure(QL1S("##") + body["attachmentId"].toString(), + msg.m_enclosures.append(Enclosure(filename + QL1S(GMAIL_ATTACHMENT_SEP) + body["attachmentId"].toString(), filename + QString(" (%1 KB)").arg(QString::number(body["size"].toInt() / 1000.0)))); } } diff --git a/src/services/gmail/network/gmailnetworkfactory.h b/src/services/gmail/network/gmailnetworkfactory.h index 1db764e20..6ad49bc2e 100755 --- a/src/services/gmail/network/gmailnetworkfactory.h +++ b/src/services/gmail/network/gmailnetworkfactory.h @@ -15,6 +15,7 @@ class RootItem; class GmailServiceRoot; class OAuth2Service; +class Downloader; class GmailNetworkFactory : public QObject { Q_OBJECT @@ -38,6 +39,8 @@ class GmailNetworkFactory : public QObject { // Returned items do not have primary IDs assigned. //RootItem* feedsCategories(); + Downloader* downloadAttachment(const QString& attachment_id); + QList messages(const QString& stream_id, Feed::Status& error); void markMessagesRead(RootItem::ReadStatus status, const QStringList& custom_ids, bool async = true); void markMessagesStarred(RootItem::Importance importance, const QStringList& custom_ids, bool async = true); diff --git a/src/services/standard/gui/formstandardimportexport.cpp b/src/services/standard/gui/formstandardimportexport.cpp index da15e5def..9d4f4fc2e 100755 --- a/src/services/standard/gui/formstandardimportexport.cpp +++ b/src/services/standard/gui/formstandardimportexport.cpp @@ -257,7 +257,7 @@ void FormStandardImportExport::exportFeeds() { if (result_export) { try { - IOFactory::writeTextFile(m_ui->m_lblSelectFile->label()->text(), result_data); + IOFactory::writeFile(m_ui->m_lblSelectFile->label()->text(), result_data); m_ui->m_lblResult->setStatus(WidgetWithStatus::Ok, tr("Feeds were exported successfully."), tr("Feeds were exported successfully.")); } catch (IOException& ex) { diff --git a/src/services/standard/standardserviceroot.cpp b/src/services/standard/standardserviceroot.cpp index 916e3c995..b4c2231e9 100755 --- a/src/services/standard/standardserviceroot.cpp +++ b/src/services/standard/standardserviceroot.cpp @@ -63,7 +63,7 @@ void StandardServiceRoot::start(bool freshly_activated) { QString output_msg; try { - model.importAsOPML20(IOFactory::readTextFile(file_to_load), false); + model.importAsOPML20(IOFactory::readFile(file_to_load), false); model.checkAllItems(); if (mergeImportExportModel(&model, this, output_msg)) { diff --git a/src/services/tt-rss/network/ttrssnetworkfactory.cpp b/src/services/tt-rss/network/ttrssnetworkfactory.cpp index 4a53c2b18..700cbc15b 100755 --- a/src/services/tt-rss/network/ttrssnetworkfactory.cpp +++ b/src/services/tt-rss/network/ttrssnetworkfactory.cpp @@ -225,7 +225,7 @@ TtRssGetHeadlinesResponse TtRssNetworkFactory::getHeadlines(int feed_id, int lim result = TtRssGetHeadlinesResponse(QString::fromUtf8(result_raw)); } - IOFactory::writeTextFile("aaa", result_raw); + IOFactory::writeFile("aaa", result_raw); if (network_reply.first != QNetworkReply::NoError) { qWarning("TT-RSS: getHeadlines failed with error %d.", network_reply.first);