File indexing completed on 2024-04-14 04:52:49

0001 /*
0002     This file is part of Akregator.
0003 
0004     SPDX-FileCopyrightText: 2004 Teemu Rytilahti <tpr@d5k.net>
0005     SPDX-FileCopyrightText: 2023 Stefano Crocco <stefano.crocco@alice.it>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-Qt-exception
0008 */
0009 
0010 #include "konqfeedicon.h"
0011 
0012 #include "pluginutil.h"
0013 #include "akregatorplugindebug.h"
0014 
0015 #include <kpluginfactory.h>
0016 #include <KLocalizedString>
0017 #include <kiconloader.h>
0018 #include <kparts/part.h>
0019 #include <kparts/statusbarextension.h>
0020 #include <KParts/ReadOnlyPart>
0021 #include "kf5compat.h" //For NavigationExtension
0022 #include <kio/job.h>
0023 #include <kurllabel.h>
0024 #include <kprotocolinfo.h>
0025 #include <KCharsets>
0026 
0027 #include <QApplication>
0028 #include <QStatusBar>
0029 #include <QStyle>
0030 #include <QClipboard>
0031 #include <QWidgetAction>
0032 #include <QInputDialog>
0033 
0034 #include <htmlextension.h>
0035 #include <browserarguments.h>
0036 #include <browserextension.h>
0037 
0038 using namespace Akregator;
0039 
0040 K_PLUGIN_CLASS_WITH_JSON(KonqFeedIcon, "akregator_konqfeedicon.json")
0041 
0042 static QUrl baseUrl(KParts::ReadOnlyPart *part)
0043 {
0044     QUrl url;
0045     HtmlExtension *ext = HtmlExtension::childObject(part);
0046     if (ext) {
0047         url = ext->baseUrl();
0048     }
0049     return url;
0050 }
0051 
0052 static QString query() {
0053     QString s_query = QStringLiteral("head > link[rel='alternate']");
0054     return s_query;
0055 }
0056 
0057 KonqFeedIcon::KonqFeedIcon(QObject *parent, const QVariantList &args)
0058     : KonqParts::Plugin(parent),
0059       m_part(nullptr),
0060       m_feedIcon(nullptr),
0061       m_statusBarEx(nullptr),
0062       m_menu(nullptr)
0063 {
0064     Q_UNUSED(args);
0065 
0066     // make our icon foundable by the KIconLoader
0067     KIconLoader::global()->addAppDir(QStringLiteral("akregator"));
0068 
0069     KParts::ReadOnlyPart *part = qobject_cast<KParts::ReadOnlyPart *>(parent);
0070     if (part) {
0071         HtmlExtension *ext = HtmlExtension::childObject(part);
0072 #if QT_VERSION_MAJOR < 6
0073         KParts::SelectorInterface *syncSelectorInterface = qobject_cast<KParts::SelectorInterface *>(ext);
0074 #else
0075         AsyncSelectorInterface *syncSelectorInterface = nullptr;
0076 #endif
0077         AsyncSelectorInterface *asyncSelectorInterface = qobject_cast<AsyncSelectorInterface*>(ext);
0078         if (syncSelectorInterface || asyncSelectorInterface) {
0079             m_part = part;
0080 #if QT_VERSION_MAJOR < 6
0081             auto slot = syncSelectorInterface ? &KonqFeedIcon::updateFeedIcon : &KonqFeedIcon::updateFeedIconAsync;
0082 #else
0083             auto slot = &KonqFeedIcon::updateFeedIconAsync;
0084 #endif
0085             connect(m_part, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, slot);
0086             connect(m_part, &KParts::ReadOnlyPart::completedWithPendingAction, this, slot);
0087             connect(m_part, &KParts::ReadOnlyPart::started, this, &KonqFeedIcon::removeFeedIcon);
0088         }
0089     }
0090 }
0091 
0092 KonqFeedIcon::~KonqFeedIcon()
0093 {
0094     //When the part is destroyed, this becomes nullptr before this destructor is called
0095     if (m_part) {
0096         m_statusBarEx = KParts::StatusBarExtension::childObject(m_part);
0097         if (m_statusBarEx) {
0098             m_statusBarEx->removeStatusBarItem(m_feedIcon);
0099         }
0100     }
0101     delete m_feedIcon;
0102     m_feedIcon = nullptr;
0103     delete m_menu;
0104     m_menu = nullptr;
0105 }
0106 
0107 bool Akregator::KonqFeedIcon::isUrlUsable() const
0108 {
0109     // Ensure that it is safe to use the URL, before doing anything else with it
0110     const QUrl partUrl(m_part->url());
0111     if (!partUrl.isValid() || partUrl.scheme().isEmpty()) {
0112         return false;
0113     }
0114     // Since attempting to determine feed info for about:blank crashes khtml,
0115     // lets prevent such look up for local urls (about, file, man, etc...)
0116     if (KProtocolInfo::protocolClass(partUrl.scheme()).compare(QLatin1String(":local"), Qt::CaseInsensitive) == 0) {
0117         return false;
0118     }
0119     return true;
0120 }
0121 
0122 QAction * Akregator::KonqFeedIcon::actionTitleForFeed(const QString &title, QWidget* parent)
0123 {
0124     QLabel *l = new QLabel(title);
0125     l->setAlignment(Qt::AlignCenter);
0126     QWidgetAction *wa = new QWidgetAction(parent);
0127     wa->setDefaultWidget(l);
0128     return wa;
0129 }
0130 
0131 QMenu * Akregator::KonqFeedIcon::createMenuForFeed(const Feed& feed, QWidget* parent, bool addSection)
0132 {
0133     QMenu *menu = new QMenu(feed.title(), parent);
0134     if (addSection) {
0135         menu->addAction(actionTitleForFeed(feed.title(), menu));
0136         menu->addSeparator();
0137     }
0138     menu->addAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add feed to Akregator"), menu, [feed, this](){addFeedToAkregator(feed.url());});
0139     menu->addAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18n("Copy feed URL to clipboard"), menu, [feed, this](){copyFeedUrlToClipboard(feed.url());});
0140     menu->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("Open feed URL"), menu, [feed, this](){openFeedUrl(feed.url(), feed.mimeType());});
0141     return menu;
0142 }
0143 
0144 void KonqFeedIcon::contextMenu()
0145 {
0146     delete m_menu;
0147     if (m_feedList.count() == 1) {
0148         m_menu = createMenuForFeed(m_feedList.first(), m_part->widget(), true);
0149     } else {
0150         m_menu = new QMenu(m_part->widget());
0151         m_menu->addAction(actionTitleForFeed(i18nc("@title:menu title for the feeds menu", "Feeds"), m_menu));
0152         m_menu->addSeparator();
0153         for (const Feed &f : m_feedList) {
0154             m_menu->addMenu(createMenuForFeed(f, m_menu));
0155             m_menu->addSeparator();
0156         }
0157         m_menu->addAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add All Found Feeds to Akregator"), this, &KonqFeedIcon::addAllFeeds);
0158     }
0159     m_menu->popup(QCursor::pos());
0160 }
0161 
0162 void Akregator::KonqFeedIcon::updateFeedIconAsync()
0163 {
0164     if (!isUrlUsable() || m_feedIcon) {
0165         return;
0166     }
0167 
0168     AsyncSelectorInterface *asyncIface = qobject_cast<AsyncSelectorInterface*>(HtmlExtension::childObject(m_part));
0169     if (!asyncIface) {
0170         return;
0171     }
0172 
0173     auto callback = [this](const QList<Element> &nodes) {
0174         fillFeedList(nodes);
0175         if (!m_feedList.isEmpty()) {
0176             addFeedIcon();
0177         }
0178     };
0179     asyncIface->querySelectorAllAsync(query(), AsyncSelectorInterface::EntireContent, callback);
0180 }
0181 
0182 #if QT_VERSION_MAJOR < 6
0183 void KonqFeedIcon::updateFeedIcon()
0184 {
0185     if (!isUrlUsable() || m_feedIcon) {
0186         return;
0187     }
0188 
0189     HtmlExtension *ext = HtmlExtension::childObject(m_part);
0190     KParts::SelectorInterface *syncInterface = qobject_cast<KParts::SelectorInterface *>(ext);
0191     QList<KParts::SelectorInterface::Element> linkNodes = syncInterface->querySelectorAll(query(), KParts::SelectorInterface::EntireContent);
0192     fillFeedList(linkNodes);
0193     if (m_feedList.isEmpty()) {
0194         return;
0195     }
0196     addFeedIcon();
0197 }
0198 #endif
0199 
0200 void Akregator::KonqFeedIcon::fillFeedList(const QList<Element> &linkNodes)
0201 {
0202     QString doc;
0203     for (const Element &e : linkNodes) {
0204         QString rel = e.attribute(QStringLiteral("rel")).toLower();
0205         if (!rel.endsWith(QStringLiteral("alternate")) && !rel.endsWith(QStringLiteral("feed")) && !rel.endsWith(QStringLiteral("service.feed"))) {
0206             continue;
0207         }
0208 
0209         static const QStringList acceptableMimeTypes =  {
0210             QStringLiteral("application/rss+xml"),
0211             QStringLiteral("application/rdf+xml"),
0212             QStringLiteral("application/atom+xml"),
0213             QStringLiteral("application/xml")
0214         };
0215         QString mimeType = e.attribute("type").toLower();
0216         if (!acceptableMimeTypes.contains(mimeType)) {
0217             continue;
0218         }
0219         QString url = KCharsets::resolveEntities(e.attribute(QStringLiteral("href")));
0220         if (url.isEmpty()) {
0221             continue;
0222         }
0223         url = PluginUtil::fixRelativeURL(url, baseUrl(m_part));
0224 
0225         QString title = KCharsets::resolveEntities(e.attribute(QStringLiteral("title")));
0226         if (title.isEmpty()) {
0227             title = url;
0228         }
0229         m_feedList.append(Feed(url, title, mimeType));
0230     }
0231 }
0232 
0233 void Akregator::KonqFeedIcon::addFeedIcon()
0234 {
0235     m_statusBarEx = KParts::StatusBarExtension::childObject(m_part);
0236     if (!m_statusBarEx) {
0237         return;
0238     }
0239 
0240     m_feedIcon = new KUrlLabel(m_statusBarEx->statusBar());
0241 
0242     // from khtmlpart's ualabel
0243     m_feedIcon->setFixedHeight(qApp->style()->pixelMetric(QStyle::PM_SmallIconSize));
0244     m_feedIcon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
0245     m_feedIcon->setUseCursor(false);
0246 
0247     m_feedIcon->setPixmap(KIconLoader::global()->loadIcon(QStringLiteral("feed"), KIconLoader::User));
0248     m_feedIcon->setToolTip(i18n("Subscribe to site updates (using news feed)"));
0249 
0250     m_statusBarEx->addStatusBarItem(m_feedIcon, 0, true);
0251 
0252     connect(m_feedIcon, QOverload<>::of(&KUrlLabel::leftClickedUrl), this, &KonqFeedIcon::contextMenu);
0253 }
0254 
0255 void KonqFeedIcon::removeFeedIcon()
0256 {
0257     m_feedList.clear();
0258     if (m_feedIcon && m_statusBarEx) {
0259         m_statusBarEx->removeStatusBarItem(m_feedIcon);
0260         delete m_feedIcon;
0261         m_feedIcon = nullptr;
0262     }
0263 
0264     // Close the popup if it's open, otherwise we crash
0265     delete m_menu;
0266     m_menu = nullptr;
0267 }
0268 
0269 // from akregatorplugin.cpp
0270 void KonqFeedIcon::addAllFeeds()
0271 {
0272     QStringList list;
0273     std::transform(m_feedList.constBegin(), m_feedList.constEnd(), std::back_inserter(list), [](const Feed &f){return f.url();});
0274     PluginUtil::addFeeds(list);
0275 }
0276 
0277 void Akregator::KonqFeedIcon::addFeedToAkregator(const QString& url)
0278 {
0279     PluginUtil::addFeeds({url});
0280 }
0281 
0282 void Akregator::KonqFeedIcon::copyFeedUrlToClipboard(const QString& url)
0283 {
0284     QApplication::clipboard()->setText(url);
0285 }
0286 
0287 void Akregator::KonqFeedIcon::openFeedUrl(const QString& url, const QString &mimeType)
0288 {
0289     KParts::NavigationExtension *ext = KParts::NavigationExtension::childObject(m_part);
0290     if (!ext) {
0291         return;
0292     }
0293     KParts::OpenUrlArguments args;
0294     args.setMimeType(mimeType);
0295     BrowserArguments bargs;
0296     bargs.setNewTab(true);
0297 
0298 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0299     emit ext->openUrlRequest(QUrl(url), args, bargs);
0300 #else
0301     if (auto browserExtension = qobject_cast<BrowserExtension *>(ext)) {
0302         emit browserExtension->browserOpenUrlRequest(QUrl(url), args, bargs);
0303     } else {
0304         emit ext->openUrlRequest(QUrl(url));
0305     }
0306 #endif
0307 
0308 }
0309 
0310 #include "konqfeedicon.moc"