Allow to globally disable the use of proxy
[qBittorrent.git] / src / base / search / searchpluginmanager.cpp
blobd927006d28e9fc9974d0cea74e02d8df1385d6ba
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "searchpluginmanager.h"
32 #include <memory>
34 #include <QDir>
35 #include <QDirIterator>
36 #include <QDomDocument>
37 #include <QDomElement>
38 #include <QDomNode>
39 #include <QFile>
40 #include <QPointer>
41 #include <QProcess>
42 #include <QUrl>
44 #include "base/global.h"
45 #include "base/logger.h"
46 #include "base/net/downloadmanager.h"
47 #include "base/net/proxyconfigurationmanager.h"
48 #include "base/preferences.h"
49 #include "base/profile.h"
50 #include "base/utils/bytearray.h"
51 #include "base/utils/foreignapps.h"
52 #include "base/utils/fs.h"
53 #include "searchdownloadhandler.h"
54 #include "searchhandler.h"
56 namespace
58 void clearPythonCache(const Path &path)
60 // remove python cache artifacts in `path` and subdirs
62 PathList dirs = {path};
63 QDirIterator iter {path.data(), (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories};
64 while (iter.hasNext())
65 dirs += Path(iter.next());
67 for (const Path &dir : asConst(dirs))
69 // python 3: remove "__pycache__" folders
70 if (dir.filename() == u"__pycache__")
72 Utils::Fs::removeDirRecursively(dir);
73 continue;
76 // python 2: remove "*.pyc" files
77 const QStringList files = QDir(dir.data()).entryList(QDir::Files);
78 for (const QString &file : files)
80 const Path path {file};
81 if (path.hasExtension(u".pyc"_s))
82 Utils::Fs::removeFile(path);
88 QPointer<SearchPluginManager> SearchPluginManager::m_instance = nullptr;
90 SearchPluginManager::SearchPluginManager()
91 : m_updateUrl(u"https://searchplugins.qbittorrent.org/nova3/engines/"_s)
93 Q_ASSERT(!m_instance); // only one instance is allowed
94 m_instance = this;
96 connect(Net::ProxyConfigurationManager::instance(), &Net::ProxyConfigurationManager::proxyConfigurationChanged
97 , this, &SearchPluginManager::applyProxySettings);
98 connect(Preferences::instance(), &Preferences::changed
99 , this, &SearchPluginManager::applyProxySettings);
100 applyProxySettings();
102 updateNova();
103 update();
106 SearchPluginManager::~SearchPluginManager()
108 qDeleteAll(m_plugins);
111 SearchPluginManager *SearchPluginManager::instance()
113 if (!m_instance)
114 m_instance = new SearchPluginManager;
115 return m_instance;
118 void SearchPluginManager::freeInstance()
120 delete m_instance;
123 QStringList SearchPluginManager::allPlugins() const
125 return m_plugins.keys();
128 QStringList SearchPluginManager::enabledPlugins() const
130 QStringList plugins;
131 for (const PluginInfo *plugin : asConst(m_plugins))
133 if (plugin->enabled)
134 plugins << plugin->name;
137 return plugins;
140 QStringList SearchPluginManager::supportedCategories() const
142 QStringList result;
143 for (const PluginInfo *plugin : asConst(m_plugins))
145 if (plugin->enabled)
147 for (const QString &cat : plugin->supportedCategories)
149 if (!result.contains(cat))
150 result << cat;
155 return result;
158 QStringList SearchPluginManager::getPluginCategories(const QString &pluginName) const
160 QStringList plugins;
161 if (pluginName == u"all")
162 plugins = allPlugins();
163 else if ((pluginName == u"enabled") || (pluginName == u"multi"))
164 plugins = enabledPlugins();
165 else
166 plugins << pluginName.trimmed();
168 QSet<QString> categories;
169 for (const QString &name : asConst(plugins))
171 const PluginInfo *plugin = pluginInfo(name);
172 if (!plugin) continue; // plugin wasn't found
173 for (const QString &category : plugin->supportedCategories)
174 categories << category;
177 return categories.values();
180 PluginInfo *SearchPluginManager::pluginInfo(const QString &name) const
182 return m_plugins.value(name);
185 void SearchPluginManager::enablePlugin(const QString &name, const bool enabled)
187 PluginInfo *plugin = m_plugins.value(name, nullptr);
188 if (plugin)
190 plugin->enabled = enabled;
191 // Save to Hard disk
192 Preferences *const pref = Preferences::instance();
193 QStringList disabledPlugins = pref->getSearchEngDisabled();
194 if (enabled)
195 disabledPlugins.removeAll(name);
196 else if (!disabledPlugins.contains(name))
197 disabledPlugins.append(name);
198 pref->setSearchEngDisabled(disabledPlugins);
200 emit pluginEnabled(name, enabled);
204 // Updates shipped plugin
205 void SearchPluginManager::updatePlugin(const QString &name)
207 installPlugin(u"%1%2.py"_s.arg(m_updateUrl, name));
210 // Install or update plugin from file or url
211 void SearchPluginManager::installPlugin(const QString &source)
213 clearPythonCache(engineLocation());
215 if (Net::DownloadManager::hasSupportedScheme(source))
217 using namespace Net;
218 DownloadManager::instance()->download(DownloadRequest(source).saveToFile(true)
219 , Preferences::instance()->useProxyForGeneralPurposes()
220 , this, &SearchPluginManager::pluginDownloadFinished);
222 else
224 const Path path {source.startsWith(u"file:", Qt::CaseInsensitive) ? QUrl(source).toLocalFile() : source};
226 QString pluginName = path.filename();
227 if (pluginName.endsWith(u".py", Qt::CaseInsensitive))
229 pluginName.chop(pluginName.size() - pluginName.lastIndexOf(u'.'));
230 installPlugin_impl(pluginName, path);
232 else
234 emit pluginInstallationFailed(pluginName, tr("Unknown search engine plugin file format."));
239 void SearchPluginManager::installPlugin_impl(const QString &name, const Path &path)
241 const PluginVersion newVersion = getPluginVersion(path);
242 const PluginInfo *plugin = pluginInfo(name);
243 if (plugin && !(plugin->version < newVersion))
245 LogMsg(tr("Plugin already at version %1, which is greater than %2").arg(plugin->version.toString(), newVersion.toString()), Log::INFO);
246 emit pluginUpdateFailed(name, tr("A more recent version of this plugin is already installed."));
247 return;
250 // Process with install
251 const Path destPath = pluginPath(name);
252 const Path backupPath = destPath + u".bak";
253 bool updated = false;
254 if (destPath.exists())
256 // Backup in case install fails
257 Utils::Fs::copyFile(destPath, backupPath);
258 Utils::Fs::removeFile(destPath);
259 updated = true;
261 // Copy the plugin
262 Utils::Fs::copyFile(path, destPath);
263 // Update supported plugins
264 update();
265 // Check if this was correctly installed
266 if (!m_plugins.contains(name))
268 // Remove broken file
269 Utils::Fs::removeFile(destPath);
270 LogMsg(tr("Plugin %1 is not supported.").arg(name), Log::INFO);
271 if (updated)
273 // restore backup
274 Utils::Fs::copyFile(backupPath, destPath);
275 Utils::Fs::removeFile(backupPath);
276 // Update supported plugins
277 update();
278 emit pluginUpdateFailed(name, tr("Plugin is not supported."));
280 else
282 emit pluginInstallationFailed(name, tr("Plugin is not supported."));
285 else
287 // Install was successful, remove backup
288 if (updated)
290 LogMsg(tr("Plugin %1 has been successfully updated.").arg(name), Log::INFO);
291 Utils::Fs::removeFile(backupPath);
296 bool SearchPluginManager::uninstallPlugin(const QString &name)
298 clearPythonCache(engineLocation());
300 // remove it from hard drive
301 const Path pluginsPath = pluginsLocation();
302 const QStringList filters {name + u".*"};
303 const QStringList files = QDir(pluginsPath.data()).entryList(filters, QDir::Files, QDir::Unsorted);
304 for (const QString &file : files)
305 Utils::Fs::removeFile(pluginsPath / Path(file));
306 // Remove it from supported engines
307 delete m_plugins.take(name);
309 emit pluginUninstalled(name);
310 return true;
313 void SearchPluginManager::updateIconPath(PluginInfo *const plugin)
315 if (!plugin) return;
317 const Path pluginsPath = pluginsLocation();
318 Path iconPath = pluginsPath / Path(plugin->name + u".png");
319 if (iconPath.exists())
321 plugin->iconPath = iconPath;
323 else
325 iconPath = pluginsPath / Path(plugin->name + u".ico");
326 if (iconPath.exists())
327 plugin->iconPath = iconPath;
331 void SearchPluginManager::checkForUpdates()
333 // Download version file from update server
334 using namespace Net;
335 DownloadManager::instance()->download({m_updateUrl + u"versions.txt"}
336 , Preferences::instance()->useProxyForGeneralPurposes()
337 , this, &SearchPluginManager::versionInfoDownloadFinished);
340 SearchDownloadHandler *SearchPluginManager::downloadTorrent(const QString &siteUrl, const QString &url)
342 return new SearchDownloadHandler {siteUrl, url, this};
345 SearchHandler *SearchPluginManager::startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins)
347 // No search pattern entered
348 Q_ASSERT(!pattern.isEmpty());
350 return new SearchHandler {pattern, category, usedPlugins, this};
353 QString SearchPluginManager::categoryFullName(const QString &categoryName)
355 const QHash<QString, QString> categoryTable
357 {u"all"_s, tr("All categories")},
358 {u"movies"_s, tr("Movies")},
359 {u"tv"_s, tr("TV shows")},
360 {u"music"_s, tr("Music")},
361 {u"games"_s, tr("Games")},
362 {u"anime"_s, tr("Anime")},
363 {u"software"_s, tr("Software")},
364 {u"pictures"_s, tr("Pictures")},
365 {u"books"_s, tr("Books")}
367 return categoryTable.value(categoryName);
370 QString SearchPluginManager::pluginFullName(const QString &pluginName) const
372 return pluginInfo(pluginName) ? pluginInfo(pluginName)->fullName : QString();
375 Path SearchPluginManager::pluginsLocation()
377 return (engineLocation() / Path(u"engines"_s));
380 Path SearchPluginManager::engineLocation()
382 static Path location;
383 if (location.isEmpty())
385 location = specialFolderLocation(SpecialFolder::Data) / Path(u"nova3"_s);
386 Utils::Fs::mkpath(location);
389 return location;
392 void SearchPluginManager::applyProxySettings()
394 const auto *proxyManager = Net::ProxyConfigurationManager::instance();
395 const Net::ProxyConfiguration proxyConfig = proxyManager->proxyConfiguration();
397 // Define environment variables for urllib in search engine plugins
398 QString proxyStrHTTP, proxyStrSOCK;
399 if ((proxyConfig.type != Net::ProxyType::None) && Preferences::instance()->useProxyForGeneralPurposes())
401 switch (proxyConfig.type)
403 case Net::ProxyType::HTTP:
404 if (proxyConfig.authEnabled)
406 proxyStrHTTP = u"http://%1:%2@%3:%4"_s.arg(proxyConfig.username
407 , proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port));
409 else
411 proxyStrHTTP = u"http://%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port));
413 break;
415 case Net::ProxyType::SOCKS5:
416 if (proxyConfig.authEnabled)
418 proxyStrSOCK = u"%1:%2@%3:%4"_s.arg(proxyConfig.username
419 , proxyConfig.password, proxyConfig.ip, QString::number(proxyConfig.port));
421 else
423 proxyStrSOCK = u"%1:%2"_s.arg(proxyConfig.ip, QString::number(proxyConfig.port));
425 break;
427 default:
428 qDebug("Disabling HTTP communications proxy");
431 qDebug("HTTP communications proxy string: %s"
432 , qUtf8Printable((proxyConfig.type == Net::ProxyType::SOCKS5) ? proxyStrSOCK : proxyStrHTTP));
435 qputenv("http_proxy", proxyStrHTTP.toLocal8Bit());
436 qputenv("https_proxy", proxyStrHTTP.toLocal8Bit());
437 qputenv("sock_proxy", proxyStrSOCK.toLocal8Bit());
440 void SearchPluginManager::versionInfoDownloadFinished(const Net::DownloadResult &result)
442 if (result.status == Net::DownloadStatus::Success)
443 parseVersionInfo(result.data);
444 else
445 emit checkForUpdatesFailed(tr("Update server is temporarily unavailable. %1").arg(result.errorString));
448 void SearchPluginManager::pluginDownloadFinished(const Net::DownloadResult &result)
450 if (result.status == Net::DownloadStatus::Success)
452 const Path filePath = result.filePath;
454 const auto pluginPath = Path(QUrl(result.url).path()).removedExtension();
455 installPlugin_impl(pluginPath.filename(), filePath);
456 Utils::Fs::removeFile(filePath);
458 else
460 const QString url = result.url;
461 QString pluginName = url.mid(url.lastIndexOf(u'/') + 1);
462 pluginName.replace(u".py"_s, u""_s, Qt::CaseInsensitive);
464 if (pluginInfo(pluginName))
465 emit pluginUpdateFailed(pluginName, tr("Failed to download the plugin file. %1").arg(result.errorString));
466 else
467 emit pluginInstallationFailed(pluginName, tr("Failed to download the plugin file. %1").arg(result.errorString));
471 // Update nova.py search plugin if necessary
472 void SearchPluginManager::updateNova()
474 // create nova directory if necessary
475 const Path enginePath = engineLocation();
477 QFile packageFile {(enginePath / Path(u"__init__.py"_s)).data()};
478 packageFile.open(QIODevice::WriteOnly);
479 packageFile.close();
481 Utils::Fs::mkdir(enginePath / Path(u"engines"_s));
483 QFile packageFile2 {(enginePath / Path(u"engines/__init__.py"_s)).data()};
484 packageFile2.open(QIODevice::WriteOnly);
485 packageFile2.close();
487 // Copy search plugin files (if necessary)
488 const auto updateFile = [&enginePath](const Path &filename, const bool compareVersion)
490 const Path filePathBundled = Path(u":/searchengine/nova3"_s) / filename;
491 const Path filePathDisk = enginePath / filename;
493 if (compareVersion && (getPluginVersion(filePathBundled) <= getPluginVersion(filePathDisk)))
494 return;
496 Utils::Fs::removeFile(filePathDisk);
497 Utils::Fs::copyFile(filePathBundled, filePathDisk);
500 updateFile(Path(u"helpers.py"_s), true);
501 updateFile(Path(u"nova2.py"_s), true);
502 updateFile(Path(u"nova2dl.py"_s), true);
503 updateFile(Path(u"novaprinter.py"_s), true);
504 updateFile(Path(u"socks.py"_s), false);
507 void SearchPluginManager::update()
509 QProcess nova;
510 nova.setProcessEnvironment(QProcessEnvironment::systemEnvironment());
512 const QStringList params
514 Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG,
515 (engineLocation() / Path(u"/nova2.py"_s)).toString(),
516 u"--capabilities"_s
518 nova.start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly);
519 nova.waitForFinished();
521 const auto capabilities = QString::fromUtf8(nova.readAllStandardOutput());
522 QDomDocument xmlDoc;
523 if (!xmlDoc.setContent(capabilities))
525 qWarning() << "Could not parse Nova search engine capabilities, msg: " << capabilities.toLocal8Bit().data();
526 qWarning() << "Error: " << nova.readAllStandardError().constData();
527 return;
530 const QDomElement root = xmlDoc.documentElement();
531 if (root.tagName() != u"capabilities")
533 qWarning() << "Invalid XML file for Nova search engine capabilities, msg: " << capabilities.toLocal8Bit().data();
534 return;
537 for (QDomNode engineNode = root.firstChild(); !engineNode.isNull(); engineNode = engineNode.nextSibling())
539 const QDomElement engineElem = engineNode.toElement();
540 if (!engineElem.isNull())
542 const QString pluginName = engineElem.tagName();
544 auto plugin = std::make_unique<PluginInfo>();
545 plugin->name = pluginName;
546 plugin->version = getPluginVersion(pluginPath(pluginName));
547 plugin->fullName = engineElem.elementsByTagName(u"name"_s).at(0).toElement().text();
548 plugin->url = engineElem.elementsByTagName(u"url"_s).at(0).toElement().text();
550 const QStringList categories = engineElem.elementsByTagName(u"categories"_s).at(0).toElement().text().split(u' ');
551 for (QString cat : categories)
553 cat = cat.trimmed();
554 if (!cat.isEmpty())
555 plugin->supportedCategories << cat;
558 const QStringList disabledEngines = Preferences::instance()->getSearchEngDisabled();
559 plugin->enabled = !disabledEngines.contains(pluginName);
561 updateIconPath(plugin.get());
563 if (!m_plugins.contains(pluginName))
565 m_plugins[pluginName] = plugin.release();
566 emit pluginInstalled(pluginName);
568 else if (m_plugins[pluginName]->version != plugin->version)
570 delete m_plugins.take(pluginName);
571 m_plugins[pluginName] = plugin.release();
572 emit pluginUpdated(pluginName);
578 void SearchPluginManager::parseVersionInfo(const QByteArray &info)
580 QHash<QString, PluginVersion> updateInfo;
581 int numCorrectData = 0;
583 const QVector<QByteArray> lines = Utils::ByteArray::splitToViews(info, "\n", Qt::SkipEmptyParts);
584 for (QByteArray line : lines)
586 line = line.trimmed();
587 if (line.isEmpty()) continue;
588 if (line.startsWith('#')) continue;
590 const QVector<QByteArray> list = Utils::ByteArray::splitToViews(line, ":", Qt::SkipEmptyParts);
591 if (list.size() != 2) continue;
593 const auto pluginName = QString::fromUtf8(list.first().trimmed());
594 const auto version = PluginVersion::fromString(QString::fromLatin1(list.last().trimmed()));
596 if (!version.isValid()) continue;
598 ++numCorrectData;
599 if (isUpdateNeeded(pluginName, version))
601 LogMsg(tr("Plugin \"%1\" is outdated, updating to version %2").arg(pluginName, version.toString()), Log::INFO);
602 updateInfo[pluginName] = version;
606 if (numCorrectData < lines.size())
608 emit checkForUpdatesFailed(tr("Incorrect update info received for %1 out of %2 plugins.")
609 .arg(QString::number(lines.size() - numCorrectData), QString::number(lines.size())));
611 else
613 emit checkForUpdatesFinished(updateInfo);
617 bool SearchPluginManager::isUpdateNeeded(const QString &pluginName, const PluginVersion &newVersion) const
619 const PluginInfo *plugin = pluginInfo(pluginName);
620 if (!plugin) return true;
622 PluginVersion oldVersion = plugin->version;
623 return (newVersion > oldVersion);
626 Path SearchPluginManager::pluginPath(const QString &name)
628 return (pluginsLocation() / Path(name + u".py"));
631 PluginVersion SearchPluginManager::getPluginVersion(const Path &filePath)
633 const int lineMaxLength = 16;
635 QFile pluginFile {filePath.data()};
636 if (!pluginFile.open(QIODevice::ReadOnly | QIODevice::Text))
637 return {};
639 while (!pluginFile.atEnd())
641 const auto line = QString::fromUtf8(pluginFile.readLine(lineMaxLength)).remove(u' ');
642 if (!line.startsWith(u"#VERSION:", Qt::CaseInsensitive)) continue;
644 const QString versionStr = line.mid(9);
645 const auto version = PluginVersion::fromString(versionStr);
646 if (version.isValid())
647 return version;
649 LogMsg(tr("Search plugin '%1' contains invalid version string ('%2')")
650 .arg(filePath.filename(), versionStr), Log::MsgType::WARNING);
651 break;
654 return {};