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"