1 /*
2     This file is part of Akregator.
3 
4     SPDX-FileCopyrightText: 2004 Teemu Rytilahti <tpr@d5k.net>
5 
6     SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-Qt-exception
7 */
8 
9 #include "konqfeedicon.h"
10 
11 #include "pluginutil.h"
12 #include "akregatorplugindebug.h"
13 
14 #include <kpluginfactory.h>
15 #include <KLocalizedString>
16 #include <kiconloader.h>
17 #include <kparts/part.h>
18 #include <kparts/statusbarextension.h>
19 #include <KParts/ReadOnlyPart>
20 #include <KParts/HtmlExtension>
21 #include <KParts/SelectorInterface>
22 #include <kio/job.h>
23 #include <kurllabel.h>
24 #include <kprotocolinfo.h>
25 
26 #include <QApplication>
27 #include <qstatusbar.h>
28 #include <QStyle>
29 
30 using namespace Akregator;
31 
K_PLUGIN_FACTORY(KonqFeedIconFactory,registerPlugin<KonqFeedIcon> ();)32 K_PLUGIN_FACTORY(KonqFeedIconFactory, registerPlugin<KonqFeedIcon>();)
33 
34 static QUrl baseUrl(KParts::ReadOnlyPart *part)
35 {
36     QUrl url;
37     KParts::HtmlExtension *ext = KParts::HtmlExtension::childObject(part);
38     if (ext) {
39         url = ext->baseUrl();
40     }
41     return url;
42 }
43 
KonqFeedIcon(QObject * parent,const QVariantList & args)44 KonqFeedIcon::KonqFeedIcon(QObject *parent, const QVariantList &args)
45     : KParts::Plugin(parent),
46       m_part(nullptr),
47       m_feedIcon(nullptr),
48       m_statusBarEx(nullptr),
49       m_menu(nullptr)
50 {
51     Q_UNUSED(args);
52 
53     // make our icon foundable by the KIconLoader
54     KIconLoader::global()->addAppDir(QStringLiteral("akregator"));
55 
56     KParts::ReadOnlyPart *part = qobject_cast<KParts::ReadOnlyPart *>(parent);
57     if (part) {
58         KParts::HtmlExtension *ext = KParts::HtmlExtension::childObject(part);
59         KParts::SelectorInterface *selectorInterface = qobject_cast<KParts::SelectorInterface *>(ext);
60         if (selectorInterface) {
61             m_part = part;
62             connect(m_part, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &KonqFeedIcon::addFeedIcon);
63             connect(m_part, QOverload<bool>::of(&KParts::ReadOnlyPart::completed), this, &KonqFeedIcon::addFeedIcon);
64             connect(m_part, &KParts::ReadOnlyPart::started, this, &KonqFeedIcon::removeFeedIcon);
65         }
66     }
67 }
68 
~KonqFeedIcon()69 KonqFeedIcon::~KonqFeedIcon()
70 {
71     m_statusBarEx = KParts::StatusBarExtension::childObject(m_part);
72     if (m_statusBarEx) {
73         m_statusBarEx->removeStatusBarItem(m_feedIcon);
74         // if the statusbar extension is deleted, the icon is deleted as well (being the child of the status bar)
75         delete m_feedIcon;
76     }
77     // the icon is deleted in every case
78     m_feedIcon = nullptr;
79     delete m_menu;
80     m_menu = nullptr;
81 }
82 
feedFound()83 bool KonqFeedIcon::feedFound()
84 {
85     // Ensure that it is safe to use the URL, before doing anything else with it
86     const QUrl partUrl(m_part->url());
87     if (!partUrl.isValid() || partUrl.scheme().isEmpty()) {
88         return false;
89     }
90     // Since attempting to determine feed info for about:blank crashes khtml,
91     // lets prevent such look up for local urls (about, file, man, etc...)
92     if (KProtocolInfo::protocolClass(partUrl.scheme()).compare(QLatin1String(":local"), Qt::CaseInsensitive) == 0) {
93         return false;
94     }
95 
96     KParts::HtmlExtension *ext = KParts::HtmlExtension::childObject(m_part);
97     KParts::SelectorInterface *selectorInterface = qobject_cast<KParts::SelectorInterface *>(ext);
98     QString doc;
99     if (selectorInterface) {
100         QList<KParts::SelectorInterface::Element> linkNodes = selectorInterface->querySelectorAll(QStringLiteral("head > link[rel=\"alternate\"]"), KParts::SelectorInterface::EntireContent);
101         for (int i = 0; i < linkNodes.count(); i++) {
102             const KParts::SelectorInterface::Element element = linkNodes.at(i);
103 
104             // TODO parse the attributes directly here, rather than re-assembling
105             // and then re-parsing in extractFromLinkTags!
106             doc += QLatin1String("<link ");
107             Q_FOREACH (const QString &attrName, element.attributeNames()) {
108                 doc += attrName + "=\"";
109                 doc += element.attribute(attrName).toHtmlEscaped().replace(QLatin1String("\""), QLatin1String("&quot;"));
110                 doc += QLatin1String("\" ");
111             }
112             doc += QLatin1String("/>");
113         }
114         qCDebug(AKREGATORPLUGIN_LOG) << doc;
115     }
116 
117     m_feedList = FeedDetector::extractFromLinkTags(doc);
118     return m_feedList.count() != 0;
119 }
120 
contextMenu()121 void KonqFeedIcon::contextMenu()
122 {
123     delete m_menu;
124     m_menu = new QMenu(m_part->widget());
125     if (m_feedList.count() == 1) {
126         m_menu->setTitle(m_feedList.first().title());
127         m_menu->addAction(QIcon::fromTheme("bookmark-new"), i18n("Add Feed to Akregator"), this, SLOT(addAllFeeds()));
128     } else {
129         m_menu->setTitle(i18n("Add Feeds to Akregator"));
130         int id = 0;
131         for (FeedDetectorEntryList::Iterator it = m_feedList.begin(); it != m_feedList.end(); ++it) {
132             QAction *action = m_menu->addAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), (*it).title(), this, SLOT(addFeed()));
133             action->setData(QVariant::fromValue(id));
134             id++;
135         }
136         //disconnect(m_menu, SIGNAL(activated(int)), this, SLOT(addFeed(int)));
137         m_menu->addSeparator();
138         m_menu->addAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add All Found Feeds to Akregator"), this, SLOT(addAllFeeds()));
139     }
140     m_menu->popup(QCursor::pos());
141 }
142 
addFeedIcon()143 void KonqFeedIcon::addFeedIcon()
144 {
145     if (!feedFound() || m_feedIcon) {
146         return;
147     }
148 
149     m_statusBarEx = KParts::StatusBarExtension::childObject(m_part);
150     if (!m_statusBarEx) {
151         return;
152     }
153 
154     m_feedIcon = new KUrlLabel(m_statusBarEx->statusBar());
155 
156     // from khtmlpart's ualabel
157     m_feedIcon->setFixedHeight(qApp->style()->pixelMetric(QStyle::PM_SmallIconSize));
158     m_feedIcon->setSizePolicy(QSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed));
159     m_feedIcon->setUseCursor(false);
160 
161     m_feedIcon->setPixmap(KIconLoader::global()->loadIcon(QStringLiteral("feed"), KIconLoader::User));
162     m_feedIcon->setToolTip(i18n("Subscribe to site updates (using news feed)"));
163 
164     m_statusBarEx->addStatusBarItem(m_feedIcon, 0, true);
165 
166     connect(m_feedIcon, QOverload<>::of(&KUrlLabel::leftClickedUrl), this, &KonqFeedIcon::contextMenu);
167 }
168 
removeFeedIcon()169 void KonqFeedIcon::removeFeedIcon()
170 {
171     m_feedList.clear();
172     if (m_feedIcon && m_statusBarEx) {
173         m_statusBarEx->removeStatusBarItem(m_feedIcon);
174         delete m_feedIcon;
175         m_feedIcon = nullptr;
176     }
177 
178     // Close the popup if it's open, otherwise we crash
179     delete m_menu;
180     m_menu = nullptr;
181 }
182 
addFeed()183 void KonqFeedIcon::addFeed()
184 {
185     bool ok = false;
186     const int id = sender() ? qobject_cast<QAction *>(sender())->data().toInt(&ok) : -1;
187     if (!ok || id == -1) {
188         return;
189     }
190     PluginUtil::addFeeds(QStringList(PluginUtil::fixRelativeURL(m_feedList[id].url(), baseUrl(m_part))));
191 }
192 
193 // from akregatorplugin.cpp
addAllFeeds()194 void KonqFeedIcon::addAllFeeds()
195 {
196     QStringList list;
197     foreach (const FeedDetectorEntry &it, m_feedList) {
198         list.append(PluginUtil::fixRelativeURL(it.url(), baseUrl(m_part)));
199     }
200     PluginUtil::addFeeds(list);
201 }
202 
203 #include "konqfeedicon.moc"
204