1 /*
2     SPDX-FileCopyrightText: 2001 Dawit Alemayehu <adawit@kde.org>
3 
4     SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6 
7 #include "uachangerplugin.h"
8 
9 #include <sys/utsname.h>
10 
11 #include <QMenu>
12 #include <QRegExp>
13 
14 #include <kwidgetsaddons_version.h>
15 #include <kactionmenu.h>
16 #include <kservicetypetrader.h>
17 #include <klocalizedstring.h>
18 #include <kservice.h>
19 #include <kconfiggroup.h>
20 #include <kpluginfactory.h>
21 #include <kprotocolmanager.h>
22 #include <kactioncollection.h>
23 #include <ksharedconfig.h>
24 
25 #include <KIO/ApplicationLauncherJob>
26 #include <KIO/JobUiDelegate>
27 #include <kparts/openurlarguments.h>
28 
29 #include <kio/job.h>
30 #include <kio/scheduler.h>
31 
K_PLUGIN_FACTORY(UAChangerPluginFactory,registerPlugin<UAChangerPlugin> ();)32 K_PLUGIN_FACTORY(UAChangerPluginFactory, registerPlugin<UAChangerPlugin>();)
33 
34 #define UA_PTOS(x) (*it)->property(x).toString()
35 #define QFL1(x) QLatin1String(x)
36 
37 UAChangerPlugin::UAChangerPlugin(QObject *parent,
38                                  const QVariantList &)
39     : KParts::Plugin(parent),
40       m_bSettingsLoaded(false), m_part(nullptr), m_config(nullptr)
41 {
42     m_pUAMenu = new KActionMenu(QIcon::fromTheme("preferences-web-browser-identification"),
43                                 i18n("Change Browser Identification"),
44                                 actionCollection());
45     actionCollection()->addAction("changeuseragent", m_pUAMenu);
46 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 77, 0)
47     m_pUAMenu->setPopupMode(QToolButton::InstantPopup);
48 #else
49     m_pUAMenu->setDelayed(false);
50 #endif
51     connect(m_pUAMenu->menu(), &QMenu::aboutToShow, this, &UAChangerPlugin::slotAboutToShow);
52 
53     if (parent!=nullptr) {
54         m_part = qobject_cast<KParts::ReadOnlyPart *>(parent);
55         connect(m_part, &KParts::ReadOnlyPart::started, this, &UAChangerPlugin::slotEnableMenu);
56         connect(m_part, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &UAChangerPlugin::slotEnableMenu);
57         connect(m_part, QOverload<bool>::of(&KParts::ReadOnlyPart::completed), this, &UAChangerPlugin::slotEnableMenu);
58     }
59 }
60 
~UAChangerPlugin()61 UAChangerPlugin::~UAChangerPlugin()
62 {
63     saveSettings();
64     slotReloadDescriptions();
65 }
66 
slotReloadDescriptions()67 void UAChangerPlugin::slotReloadDescriptions()
68 {
69     delete m_config;
70     m_config = nullptr;
71 }
72 
parseDescFiles()73 void UAChangerPlugin::parseDescFiles()
74 {
75     const KService::List list = KServiceTypeTrader::self()->query("UserAgentStrings");
76     if (list.isEmpty()) {
77         return;
78     }
79 
80     m_mapAlias.clear();
81     m_lstAlias.clear();
82     m_lstIdentity.clear();
83 
84     struct utsname utsn;
85     uname(&utsn);
86 
87     QStringList languageList = KLocalizedString::languages();
88     if (!languageList.isEmpty()) {
89         const int index = languageList.indexOf(QFL1("C"));
90         if (index > -1) {
91             if (languageList.contains(QFL1("en"))) {
92                 languageList.removeAt(index);
93             } else {
94                 languageList[index] = QFL1("en");
95             }
96         }
97     }
98 
99     KService::List::ConstIterator it = list.constBegin();
100     KService::List::ConstIterator lastItem = list.constEnd();
101 
102     for (; it != lastItem; ++it) {
103         QString ua  = UA_PTOS("X-KDE-UA-FULL");
104         QString tag = UA_PTOS("X-KDE-UA-TAG");
105 
106         // The menu groups thing by tag, with the menu name being the X-KDE-UA-NAME by default. We make groups for
107         // IE, NS, Firefox, Safari, and Opera, and put everything else into "Other"
108         QString menuName;
109         MenuGroupSortKey menuKey; // key for the group..
110         if (tag != "IE" && tag != "NN" && tag != "FF" && tag != "SAF" && tag != "OPR") {
111             tag = "OTHER";
112             menuName = i18n("Other");
113             menuKey = MenuGroupSortKey(tag, true);
114         } else {
115             menuName = UA_PTOS("X-KDE-UA-NAME");
116             menuKey  = MenuGroupSortKey(tag, false);
117         }
118 
119         if ((*it)->property("X-KDE-UA-DYNAMIC-ENTRY").toBool()) {
120             ua.replace(QFL1("appSysName"), QFL1(utsn.sysname));
121             ua.replace(QFL1("appSysRelease"), QFL1(utsn.release));
122             ua.replace(QFL1("appMachineType"), QFL1(utsn.machine));
123             ua.replace(QFL1("appLanguage"), languageList.join(QFL1(", ")));
124             ua.replace(QFL1("appPlatform"), QFL1("X11"));
125         }
126 
127         if (m_lstIdentity.contains(ua)) {
128             continue;    // Ignore dups!
129         }
130 
131         m_lstIdentity << ua;
132 
133         // Compute what to display for our menu entry --- including platform name if it's available,
134         // and avoiding repeating the browser name in categories other than 'other'.
135         QString platform = QString("%1 %2").arg(UA_PTOS("X-KDE-UA-SYSNAME")).arg(UA_PTOS("X-KDE-UA-SYSRELEASE"));
136 
137         QString alias;
138         if (platform.trimmed().isEmpty()) {
139             if (!menuKey.isOther) {
140                 alias = i18nc("%1 = browser version (e.g. 2.0)", "Version %1", UA_PTOS("X-KDE-UA-VERSION"));
141             } else
142                 alias = i18nc("%1 = browser name, %2 = browser version (e.g. Firefox, 2.0)",
143                               "%1 %2", UA_PTOS("X-KDE-UA-NAME"), UA_PTOS("X-KDE-UA-VERSION"));
144         } else {
145             if (!menuKey.isOther)
146                 alias = i18nc("%1 = browser version, %2 = platform (e.g. 2.0, Windows XP)",
147                               "Version %1 on %2", UA_PTOS("X-KDE-UA-VERSION"), platform);
148             else
149                 alias = i18nc("%1 = browser name, %2 = browser version, %3 = platform (e.g. Firefox, 2.0, Windows XP)",
150                               "%1 %2 on %3", UA_PTOS("X-KDE-UA-NAME"), UA_PTOS("X-KDE-UA-VERSION"), platform);
151         }
152 
153         m_lstAlias << alias;
154 
155         /* sort in this UA Alias alphabetically */
156         BrowserGroup ualist = m_mapAlias[menuKey];
157         BrowserGroup::Iterator e = ualist.begin();
158         while (!alias.isEmpty() && e != ualist.end()) {
159             if (m_lstAlias[(*e)] > alias) {
160                 ualist.insert(e, m_lstAlias.count() - 1);
161                 alias.clear();
162             }
163             ++e;
164         }
165 
166         if (!alias.isEmpty()) {
167             ualist.append(m_lstAlias.count() - 1);
168         }
169 
170         m_mapAlias[menuKey]   = ualist;
171         m_mapBrowser[menuKey] = menuName;
172     }
173 }
174 
slotEnableMenu()175 void UAChangerPlugin::slotEnableMenu()
176 {
177     m_currentURL = m_part->url();
178 
179     // This plugin works on local files, http[s], and webdav[s].
180     const QString proto = m_currentURL.scheme();
181     if (m_currentURL.isLocalFile() ||
182             proto.startsWith("http") || proto.startsWith("webdav")) {
183         if (!m_pUAMenu->isEnabled()) {
184             m_pUAMenu->setEnabled(true);
185         }
186     } else {
187         m_pUAMenu->setEnabled(false);
188     }
189 }
190 
slotAboutToShow()191 void UAChangerPlugin::slotAboutToShow()
192 {
193     if (!m_config) {
194         m_config = new KConfig("kio_httprc");
195         parseDescFiles();
196     }
197 
198     if (!m_bSettingsLoaded) {
199         loadSettings();
200     }
201 
202     if (m_pUAMenu->menu()->actions().isEmpty()) { // need to create the actions
203         m_defaultAction = new QAction(i18n("Default Identification"), this);
204         m_defaultAction->setCheckable(true);
205         connect(m_defaultAction, &QAction::triggered, this, &UAChangerPlugin::slotDefault);
206         m_pUAMenu->menu()->addAction(m_defaultAction);
207 
208         m_pUAMenu->menu()->addSeparator();
209 
210         m_actionGroup = new QActionGroup(m_pUAMenu->menu());
211         AliasConstIterator map = m_mapAlias.constBegin();
212         for (; map != m_mapAlias.constEnd(); ++map) {
213             QMenu *browserMenu = m_pUAMenu->menu()->addMenu(m_mapBrowser.value(map.key()));
214             BrowserGroup::ConstIterator e = map.value().begin();
215             for (; e != map.value().end(); ++e) {
216                 QAction *action = new QAction(m_lstAlias[*e], m_actionGroup);
217                 action->setCheckable(true);
218                 action->setData(*e);
219                 browserMenu->addAction(action);
220             }
221         }
222         connect(m_actionGroup, &QActionGroup::triggered, this, &UAChangerPlugin::slotItemSelected);
223 
224         m_pUAMenu->menu()->addSeparator();
225 
226         /* useless here, imho..
227            m_pUAMenu->menu()->insertItem( i18n("Reload Identifications"), this,
228            SLOT(slotReloadDescriptions()),
229            0, ++count );*/
230 
231         m_applyEntireSiteAction = new QAction(i18n("Apply to Entire Site"), this);
232         m_applyEntireSiteAction->setCheckable(true);
233         m_applyEntireSiteAction->setChecked(m_bApplyToDomain);
234         connect(m_applyEntireSiteAction, &QAction::triggered, this, &UAChangerPlugin::slotApplyToDomain);
235         m_pUAMenu->menu()->addAction(m_applyEntireSiteAction);
236 
237         m_pUAMenu->menu()->addAction(i18n("Configure..."), this, &UAChangerPlugin::slotConfigure);
238     }
239 
240     // Reflect current settings in the actions
241 
242     QString host = m_currentURL.isLocalFile() ? QFL1("localhost") : m_currentURL.host();
243     m_currentUserAgent = KProtocolManager::userAgentForHost(host);
244     //qDebug() << "User Agent: " << m_currentUserAgent;
245     m_defaultAction->setChecked(m_currentUserAgent == KProtocolManager::defaultUserAgent());
246 
247     m_applyEntireSiteAction->setChecked(m_bApplyToDomain);
248     Q_FOREACH (QAction *action, m_actionGroup->actions()) {
249         const int id = action->data().toInt();
250         action->setChecked(m_lstIdentity[id] == m_currentUserAgent);
251     }
252 
253 }
254 
slotConfigure()255 void UAChangerPlugin::slotConfigure()
256 {
257     KService::Ptr service = KService::serviceByDesktopName("useragent");
258     if (service) {
259         KIO::ApplicationLauncherJob *job = new KIO::ApplicationLauncherJob(service);
260         job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, m_part->widget()));
261         job->start();
262     }
263 }
264 
slotItemSelected(QAction * action)265 void UAChangerPlugin::slotItemSelected(QAction *action)
266 {
267     const int id = action->data().toInt();
268     if (m_lstIdentity[id] == m_currentUserAgent) {
269         return;
270     }
271 
272     m_currentUserAgent = m_lstIdentity[id];
273     QString host = m_currentURL.isLocalFile() ? QFL1("localhost") : filterHost(m_currentURL.host());
274 
275     KConfigGroup grp = m_config->group(host.toLower());
276     grp.writeEntry("UserAgent", m_currentUserAgent);
277     //qDebug() << "Writing out UserAgent=" << m_currentUserAgent << "for host=" << host;
278     grp.sync();
279 
280     // Reload the page with the new user-agent string
281     reloadPage();
282 }
283 
slotDefault()284 void UAChangerPlugin::slotDefault()
285 {
286     if (m_currentUserAgent == KProtocolManager::defaultUserAgent()) {
287         return;    // don't flicker!
288     }
289     // We have no choice but delete all higher domain level settings here since it
290     // affects what will be matched.
291     QStringList partList = m_currentURL.host().split(QLatin1Char(' '),
292 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
293                                                      Qt::SkipEmptyParts);
294 #else
295 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
296                                                      QString::SkipEmptyParts);
297 #else
298                                                      Qt::SkipEmptyParts);
299 #endif
300 #endif
301     if (!partList.isEmpty()) {
302         partList.removeFirst();
303 
304         QStringList domains;
305         // Remove the exact name match...
306         domains << m_currentURL.host();
307 
308         while (partList.count()) {
309             if (partList.count() == 2)
310                 if (partList[0].length() <= 2 && partList[1].length() == 2) {
311                     break;
312                 }
313 
314             if (partList.count() == 1) {
315                 break;
316             }
317 
318             domains << partList.join(QFL1("."));
319             partList.removeFirst();
320         }
321 
322         KConfigGroup grp(m_config, QString());
323         for (QStringList::Iterator it = domains.begin(); it != domains.end(); it++) {
324             //qDebug () << "Domain to remove: " << *it;
325             if (grp.hasGroup(*it)) {
326                 grp.deleteGroup(*it);
327             } else if (grp.hasKey(*it)) {
328                 grp.deleteEntry(*it);
329             }
330         }
331     } else if (m_currentURL.isLocalFile() && m_config->hasGroup("localhost")) {
332         m_config->deleteGroup("localhost");
333     }
334 
335     m_config->sync();
336 
337     // Reset some internal variables and inform the http io-slaves of the changes.
338     m_currentUserAgent = KProtocolManager::defaultUserAgent();
339 
340     // Reload the page with the default user-agent
341     reloadPage();
342 }
343 
reloadPage()344 void UAChangerPlugin::reloadPage()
345 {
346     // Inform running http(s) io-slaves about the change...
347     KIO::Scheduler::emitReparseSlaveConfiguration();
348 
349     KParts::OpenUrlArguments args = m_part->arguments();
350     args.setReload(true);
351     m_part->setArguments(args);
352     m_part->openUrl(m_currentURL);
353 }
354 
filterHost(const QString & hostname)355 QString UAChangerPlugin::filterHost(const QString &hostname)
356 {
357     QRegExp rx;
358 
359     // Check for IPv4 address
360     rx.setPattern("[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}");
361     if (rx.exactMatch(hostname)) {
362         return hostname;
363     }
364 
365     // Check for IPv6 address here...
366     rx.setPattern("^\\[.*\\]$");
367     if (rx.exactMatch(hostname)) {
368         return hostname;
369     }
370 
371     // Return the TLD if apply to domain or
372     return (m_bApplyToDomain ? findTLD(hostname) : hostname);
373 }
374 
findTLD(const QString & hostname)375 QString UAChangerPlugin::findTLD(const QString &hostname)
376 {
377     // As per the documentation for QUrl::topLevelDomain(), the "entire site"
378     // for a hostname is considered to be the TLD suffix as returned by that
379     // function, prefixed by the hostname component immediately before it.
380     // For example, QUrl::topLevelDomain("http://www.kde.org") gives ".org"
381     // so the returned result is "kde.org".
382 
383     QUrl u;
384     u.setScheme("http");
385     u.setHost(hostname);					// gives http://hostname/
386 
387     const QString tld = u.topLevelDomain(QUrl::EncodeUnicode);
388     if (tld.isEmpty()) return hostname;				// name has no valid TLD
389 
390     const QString prefix = hostname.chopped(tld.length());	// remaining prefix of name
391     const int idx = prefix.lastIndexOf(QLatin1Char('.'));
392     const QString prev = prefix.mid(idx+1);			// works even if no '.'
393     return prev+tld;
394 }
395 
saveSettings()396 void UAChangerPlugin::saveSettings()
397 {
398     if (!m_bSettingsLoaded) {
399         return;
400     }
401 
402     KConfig cfg("uachangerrc", KConfig::NoGlobals);
403     KConfigGroup grp = cfg.group("General");
404 
405     grp.writeEntry("applyToDomain", m_bApplyToDomain);
406 }
407 
loadSettings()408 void UAChangerPlugin::loadSettings()
409 {
410     KConfig cfg("uachangerrc", KConfig::NoGlobals);
411     KConfigGroup grp = cfg.group("General");
412 
413     m_bApplyToDomain = grp.readEntry("applyToDomain", true);
414     m_bSettingsLoaded = true;
415 }
416 
slotApplyToDomain()417 void UAChangerPlugin::slotApplyToDomain()
418 {
419     m_bApplyToDomain = !m_bApplyToDomain;
420 }
421 
422 #include "uachangerplugin.moc"
423