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