qBittorrent
searchpluginmanager.cpp
Go to the documentation of this file.
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>
5  *
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.
10  *
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.
15  *
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.
19  *
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.
28  */
29 
30 #include "searchpluginmanager.h"
31 
32 #include <memory>
33 
34 #include <QDir>
35 #include <QDirIterator>
36 #include <QDomDocument>
37 #include <QDomElement>
38 #include <QDomNode>
39 #include <QPointer>
40 #include <QProcess>
41 
42 #include "base/global.h"
43 #include "base/logger.h"
45 #include "base/preferences.h"
46 #include "base/profile.h"
47 #include "base/utils/bytearray.h"
48 #include "base/utils/foreignapps.h"
49 #include "base/utils/fs.h"
50 #include "searchdownloadhandler.h"
51 #include "searchhandler.h"
52 
53 namespace
54 {
55  void clearPythonCache(const QString &path)
56  {
57  // remove python cache artifacts in `path` and subdirs
58 
59  QStringList dirs = {path};
60  QDirIterator iter {path, (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories};
61  while (iter.hasNext())
62  dirs += iter.next();
63 
64  for (const QString &dir : asConst(dirs))
65  {
66  // python 3: remove "__pycache__" folders
67  if (dir.endsWith("/__pycache__"))
68  {
70  continue;
71  }
72 
73  // python 2: remove "*.pyc" files
74  const QStringList files = QDir(dir).entryList(QDir::Files);
75  for (const QString &file : files)
76  {
77  if (file.endsWith(".pyc"))
79  }
80  }
81  }
82 }
83 
84 QPointer<SearchPluginManager> SearchPluginManager::m_instance = nullptr;
85 
87  : m_updateUrl(QLatin1String("http://searchplugins.qbittorrent.org/nova3/engines/"))
88 {
89  Q_ASSERT(!m_instance); // only one instance is allowed
90  m_instance = this;
91 
92  updateNova();
93  update();
94 }
95 
97 {
98  qDeleteAll(m_plugins);
99 }
100 
102 {
103  if (!m_instance)
105  return m_instance;
106 }
107 
109 {
110  delete m_instance;
111 }
112 
114 {
115  return m_plugins.keys();
116 }
117 
119 {
120  QStringList plugins;
121  for (const PluginInfo *plugin : asConst(m_plugins))
122  {
123  if (plugin->enabled)
124  plugins << plugin->name;
125  }
126 
127  return plugins;
128 }
129 
131 {
132  QStringList result;
133  for (const PluginInfo *plugin : asConst(m_plugins))
134  {
135  if (plugin->enabled)
136  {
137  for (const QString &cat : plugin->supportedCategories)
138  {
139  if (!result.contains(cat))
140  result << cat;
141  }
142  }
143  }
144 
145  return result;
146 }
147 
148 QStringList SearchPluginManager::getPluginCategories(const QString &pluginName) const
149 {
150  QStringList plugins;
151  if (pluginName == "all")
152  plugins = allPlugins();
153  else if ((pluginName == "enabled") || (pluginName == "multi"))
154  plugins = enabledPlugins();
155  else
156  plugins << pluginName.trimmed();
157 
158  QSet<QString> categories;
159  for (const QString &name : asConst(plugins))
160  {
161  const PluginInfo *plugin = pluginInfo(name);
162  if (!plugin) continue; // plugin wasn't found
163  for (const QString &category : plugin->supportedCategories)
164  categories << category;
165  }
166 
167  return categories.values();
168 }
169 
170 PluginInfo *SearchPluginManager::pluginInfo(const QString &name) const
171 {
172  return m_plugins.value(name);
173 }
174 
175 void SearchPluginManager::enablePlugin(const QString &name, const bool enabled)
176 {
177  PluginInfo *plugin = m_plugins.value(name, nullptr);
178  if (plugin)
179  {
180  plugin->enabled = enabled;
181  // Save to Hard disk
182  Preferences *const pref = Preferences::instance();
183  QStringList disabledPlugins = pref->getSearchEngDisabled();
184  if (enabled)
185  disabledPlugins.removeAll(name);
186  else if (!disabledPlugins.contains(name))
187  disabledPlugins.append(name);
188  pref->setSearchEngDisabled(disabledPlugins);
189 
190  emit pluginEnabled(name, enabled);
191  }
192 }
193 
194 // Updates shipped plugin
195 void SearchPluginManager::updatePlugin(const QString &name)
196 {
197  installPlugin(QString::fromLatin1("%1%2.py").arg(m_updateUrl, name));
198 }
199 
200 // Install or update plugin from file or url
202 {
204 
206  {
207  using namespace Net;
208  DownloadManager::instance()->download(DownloadRequest(source).saveToFile(true)
210  }
211  else
212  {
213  QString path = source;
214  if (path.startsWith("file:", Qt::CaseInsensitive))
215  path = QUrl(path).toLocalFile();
216 
217  QString pluginName = Utils::Fs::fileName(path);
218  pluginName.chop(pluginName.size() - pluginName.lastIndexOf('.'));
219 
220  if (!path.endsWith(".py", Qt::CaseInsensitive))
221  emit pluginInstallationFailed(pluginName, tr("Unknown search engine plugin file format."));
222  else
223  installPlugin_impl(pluginName, path);
224  }
225 }
226 
227 void SearchPluginManager::installPlugin_impl(const QString &name, const QString &path)
228 {
229  const PluginVersion newVersion = getPluginVersion(path);
230  const PluginInfo *plugin = pluginInfo(name);
231  if (plugin && !(plugin->version < newVersion))
232  {
233  LogMsg(tr("Plugin already at version %1, which is greater than %2").arg(plugin->version, newVersion), Log::INFO);
234  emit pluginUpdateFailed(name, tr("A more recent version of this plugin is already installed."));
235  return;
236  }
237 
238  // Process with install
239  const QString destPath = pluginPath(name);
240  bool updated = false;
241  if (QFile::exists(destPath))
242  {
243  // Backup in case install fails
244  QFile::copy(destPath, destPath + ".bak");
245  Utils::Fs::forceRemove(destPath);
246  updated = true;
247  }
248  // Copy the plugin
249  QFile::copy(path, destPath);
250  // Update supported plugins
251  update();
252  // Check if this was correctly installed
253  if (!m_plugins.contains(name))
254  {
255  // Remove broken file
256  Utils::Fs::forceRemove(destPath);
257  LogMsg(tr("Plugin %1 is not supported.").arg(name), Log::INFO);
258  if (updated)
259  {
260  // restore backup
261  QFile::copy(destPath + ".bak", destPath);
262  Utils::Fs::forceRemove(destPath + ".bak");
263  // Update supported plugins
264  update();
265  emit pluginUpdateFailed(name, tr("Plugin is not supported."));
266  }
267  else
268  {
269  emit pluginInstallationFailed(name, tr("Plugin is not supported."));
270  }
271  }
272  else
273  {
274  // Install was successful, remove backup
275  if (updated)
276  {
277  LogMsg(tr("Plugin %1 has been successfully updated.").arg(name), Log::INFO);
278  Utils::Fs::forceRemove(destPath + ".bak");
279  }
280  }
281 }
282 
283 bool SearchPluginManager::uninstallPlugin(const QString &name)
284 {
286 
287  // remove it from hard drive
288  const QDir pluginsFolder(pluginsLocation());
289  QStringList filters;
290  filters << name + ".*";
291  const QStringList files = pluginsFolder.entryList(filters, QDir::Files, QDir::Unsorted);
292  for (const QString &file : files)
293  Utils::Fs::forceRemove(pluginsFolder.absoluteFilePath(file));
294  // Remove it from supported engines
295  delete m_plugins.take(name);
296 
297  emit pluginUninstalled(name);
298  return true;
299 }
300 
302 {
303  if (!plugin) return;
304  QString iconPath = QString::fromLatin1("%1/%2.png").arg(pluginsLocation(), plugin->name);
305  if (QFile::exists(iconPath))
306  {
307  plugin->iconPath = iconPath;
308  }
309  else
310  {
311  iconPath = QString::fromLatin1("%1/%2.ico").arg(pluginsLocation(), plugin->name);
312  if (QFile::exists(iconPath))
313  plugin->iconPath = iconPath;
314  }
315 }
316 
318 {
319  // Download version file from update server
320  using namespace Net;
321  DownloadManager::instance()->download({m_updateUrl + "versions.txt"}
323 }
324 
325 SearchDownloadHandler *SearchPluginManager::downloadTorrent(const QString &siteUrl, const QString &url)
326 {
327  return new SearchDownloadHandler {siteUrl, url, this};
328 }
329 
330 SearchHandler *SearchPluginManager::startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins)
331 {
332  // No search pattern entered
333  Q_ASSERT(!pattern.isEmpty());
334 
335  return new SearchHandler {pattern, category, usedPlugins, this};
336 }
337 
338 QString SearchPluginManager::categoryFullName(const QString &categoryName)
339 {
340  const QHash<QString, QString> categoryTable
341  {
342  {"all", tr("All categories")},
343  {"movies", tr("Movies")},
344  {"tv", tr("TV shows")},
345  {"music", tr("Music")},
346  {"games", tr("Games")},
347  {"anime", tr("Anime")},
348  {"software", tr("Software")},
349  {"pictures", tr("Pictures")},
350  {"books", tr("Books")}
351  };
352  return categoryTable.value(categoryName);
353 }
354 
355 QString SearchPluginManager::pluginFullName(const QString &pluginName)
356 {
357  return pluginInfo(pluginName) ? pluginInfo(pluginName)->fullName : QString();
358 }
359 
361 {
362  return QString::fromLatin1("%1/engines").arg(engineLocation());
363 }
364 
366 {
367  static QString location;
368  if (location.isEmpty())
369  {
371 
372  const QDir locationDir(location);
373  locationDir.mkpath(locationDir.absolutePath());
374  }
375 
376  return location;
377 }
378 
380 {
381  if (result.status == Net::DownloadStatus::Success)
382  parseVersionInfo(result.data);
383  else
384  emit checkForUpdatesFailed(tr("Update server is temporarily unavailable. %1").arg(result.errorString));
385 }
386 
388 {
389  if (result.status == Net::DownloadStatus::Success)
390  {
391  const QString filePath = Utils::Fs::toUniformPath(result.filePath);
392 
393  QString pluginName = Utils::Fs::fileName(result.url);
394  pluginName.chop(pluginName.size() - pluginName.lastIndexOf('.')); // Remove extension
395  installPlugin_impl(pluginName, filePath);
396  Utils::Fs::forceRemove(filePath);
397  }
398  else
399  {
400  const QString url = result.url;
401  QString pluginName = url.mid(url.lastIndexOf('/') + 1);
402  pluginName.replace(".py", "", Qt::CaseInsensitive);
403 
404  if (pluginInfo(pluginName))
405  emit pluginUpdateFailed(pluginName, tr("Failed to download the plugin file. %1").arg(result.errorString));
406  else
407  emit pluginInstallationFailed(pluginName, tr("Failed to download the plugin file. %1").arg(result.errorString));
408  }
409 }
410 
411 // Update nova.py search plugin if necessary
413 {
414  // create nova directory if necessary
415  const QDir searchDir(engineLocation());
416 
417  QFile packageFile(searchDir.absoluteFilePath("__init__.py"));
418  packageFile.open(QIODevice::WriteOnly);
419  packageFile.close();
420 
421  searchDir.mkdir("engines");
422 
423  QFile packageFile2(searchDir.absolutePath() + "/engines/__init__.py");
424  packageFile2.open(QIODevice::WriteOnly);
425  packageFile2.close();
426 
427  // Copy search plugin files (if necessary)
428  const auto updateFile = [](const QString &filename, const bool compareVersion)
429  {
430  const QString filePathBundled = ":/searchengine/nova3/" + filename;
431  const QString filePathDisk = QDir(engineLocation()).absoluteFilePath(filename);
432 
433  if (compareVersion && (getPluginVersion(filePathBundled) <= getPluginVersion(filePathDisk)))
434  return;
435 
436  Utils::Fs::forceRemove(filePathDisk);
437  QFile::copy(filePathBundled, filePathDisk);
438  };
439 
440  updateFile("helpers.py", true);
441  updateFile("nova2.py", true);
442  updateFile("nova2dl.py", true);
443  updateFile("novaprinter.py", true);
444  updateFile("sgmllib3.py", false);
445  updateFile("socks.py", false);
446 }
447 
449 {
450  QProcess nova;
451  nova.setProcessEnvironment(QProcessEnvironment::systemEnvironment());
452 
453  const QStringList params {Utils::Fs::toNativePath(engineLocation() + "/nova2.py"), "--capabilities"};
454  nova.start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly);
455  nova.waitForFinished();
456 
457  const QString capabilities = nova.readAll();
458  QDomDocument xmlDoc;
459  if (!xmlDoc.setContent(capabilities))
460  {
461  qWarning() << "Could not parse Nova search engine capabilities, msg: " << capabilities.toLocal8Bit().data();
462  qWarning() << "Error: " << nova.readAllStandardError().constData();
463  return;
464  }
465 
466  const QDomElement root = xmlDoc.documentElement();
467  if (root.tagName() != "capabilities")
468  {
469  qWarning() << "Invalid XML file for Nova search engine capabilities, msg: " << capabilities.toLocal8Bit().data();
470  return;
471  }
472 
473  for (QDomNode engineNode = root.firstChild(); !engineNode.isNull(); engineNode = engineNode.nextSibling())
474  {
475  const QDomElement engineElem = engineNode.toElement();
476  if (!engineElem.isNull())
477  {
478  const QString pluginName = engineElem.tagName();
479 
480  auto plugin = std::make_unique<PluginInfo>();
481  plugin->name = pluginName;
482  plugin->version = getPluginVersion(pluginPath(pluginName));
483  plugin->fullName = engineElem.elementsByTagName("name").at(0).toElement().text();
484  plugin->url = engineElem.elementsByTagName("url").at(0).toElement().text();
485 
486  const QStringList categories = engineElem.elementsByTagName("categories").at(0).toElement().text().split(' ');
487  for (QString cat : categories)
488  {
489  cat = cat.trimmed();
490  if (!cat.isEmpty())
491  plugin->supportedCategories << cat;
492  }
493 
494  const QStringList disabledEngines = Preferences::instance()->getSearchEngDisabled();
495  plugin->enabled = !disabledEngines.contains(pluginName);
496 
497  updateIconPath(plugin.get());
498 
499  if (!m_plugins.contains(pluginName))
500  {
501  m_plugins[pluginName] = plugin.release();
502  emit pluginInstalled(pluginName);
503  }
504  else if (m_plugins[pluginName]->version != plugin->version)
505  {
506  delete m_plugins.take(pluginName);
507  m_plugins[pluginName] = plugin.release();
508  emit pluginUpdated(pluginName);
509  }
510  }
511  }
512 }
513 
514 void SearchPluginManager::parseVersionInfo(const QByteArray &info)
515 {
516  QHash<QString, PluginVersion> updateInfo;
517  int numCorrectData = 0;
518 
519  const QVector<QByteArray> lines = Utils::ByteArray::splitToViews(info, "\n", Qt::SkipEmptyParts);
520  for (QByteArray line : lines)
521  {
522  line = line.trimmed();
523  if (line.isEmpty()) continue;
524  if (line.startsWith('#')) continue;
525 
526  const QVector<QByteArray> list = Utils::ByteArray::splitToViews(line, ":", Qt::SkipEmptyParts);
527  if (list.size() != 2) continue;
528 
529  const QString pluginName = list.first().trimmed();
530  const PluginVersion version = PluginVersion::tryParse(list.last().trimmed(), {});
531 
532  if (!version.isValid()) continue;
533 
534  ++numCorrectData;
535  if (isUpdateNeeded(pluginName, version))
536  {
537  LogMsg(tr("Plugin \"%1\" is outdated, updating to version %2").arg(pluginName, version), Log::INFO);
538  updateInfo[pluginName] = version;
539  }
540  }
541 
542  if (numCorrectData < lines.size())
543  {
544  emit checkForUpdatesFailed(tr("Incorrect update info received for %1 out of %2 plugins.")
545  .arg(QString::number(lines.size() - numCorrectData), QString::number(lines.size())));
546  }
547  else
548  {
549  emit checkForUpdatesFinished(updateInfo);
550  }
551 }
552 
553 bool SearchPluginManager::isUpdateNeeded(const QString &pluginName, const PluginVersion newVersion) const
554 {
555  const PluginInfo *plugin = pluginInfo(pluginName);
556  if (!plugin) return true;
557 
558  PluginVersion oldVersion = plugin->version;
559  return (newVersion > oldVersion);
560 }
561 
562 QString SearchPluginManager::pluginPath(const QString &name)
563 {
564  return QString::fromLatin1("%1/%2.py").arg(pluginsLocation(), name);
565 }
566 
568 {
569  QFile pluginFile(filePath);
570  if (!pluginFile.open(QIODevice::ReadOnly | QIODevice::Text))
571  return {};
572 
573  while (!pluginFile.atEnd())
574  {
575  const QString line = QString(pluginFile.readLine()).remove(' ');
576  if (!line.startsWith("#VERSION:", Qt::CaseInsensitive)) continue;
577 
578  const QString versionStr = line.mid(9);
579  const PluginVersion version = PluginVersion::tryParse(versionStr, {});
580  if (version.isValid())
581  return version;
582 
583  LogMsg(tr("Search plugin '%1' contains invalid version string ('%2')")
584  .arg(Utils::Fs::fileName(filePath), versionStr), Log::MsgType::WARNING);
585  break;
586  }
587 
588  return {};
589 }
static bool hasSupportedScheme(const QString &url)
static Preferences * instance()
QStringList getSearchEngDisabled() const
void setSearchEngDisabled(const QStringList &engines)
bool isUpdateNeeded(const QString &pluginName, PluginVersion newVersion) const
void updatePlugin(const QString &name)
void installPlugin_impl(const QString &name, const QString &path)
PluginInfo * pluginInfo(const QString &name) const
void enablePlugin(const QString &name, bool enabled=true)
QStringList enabledPlugins() const
SearchDownloadHandler * downloadTorrent(const QString &siteUrl, const QString &url)
void pluginUpdated(const QString &name)
static QString pluginPath(const QString &name)
SearchHandler * startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins)
QString pluginFullName(const QString &pluginName)
void pluginInstallationFailed(const QString &name, const QString &reason)
static QPointer< SearchPluginManager > m_instance
static PluginVersion getPluginVersion(const QString &filePath)
static void updateIconPath(PluginInfo *const plugin)
QHash< QString, PluginInfo * > m_plugins
static QString categoryFullName(const QString &categoryName)
QStringList getPluginCategories(const QString &pluginName) const
static QString engineLocation()
void pluginEnabled(const QString &name, bool enabled)
void versionInfoDownloadFinished(const Net::DownloadResult &result)
QStringList supportedCategories() const
static QString pluginsLocation()
void pluginInstalled(const QString &name)
QStringList allPlugins() const
void pluginUninstalled(const QString &name)
void installPlugin(const QString &source)
void checkForUpdatesFinished(const QHash< QString, PluginVersion > &updateInfo)
void checkForUpdatesFailed(const QString &reason)
static SearchPluginManager * instance()
bool uninstallPlugin(const QString &name)
void parseVersionInfo(const QByteArray &info)
void pluginUpdateFailed(const QString &name, const QString &reason)
void pluginDownloadFinished(const Net::DownloadResult &result)
static Version tryParse(const StringClassWithSplitMethod &s, const Version &defaultVersion)
Definition: version.h:143
constexpr bool isValid() const
Definition: version.h:126
constexpr std::add_const_t< T > & asConst(T &t) noexcept
Definition: global.h:42
flag icons free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to copy
flag icons free of to any person obtaining a copy of this software and associated documentation files(the "Software")
void LogMsg(const QString &message, const Log::MsgType &type)
Definition: logger.cpp:125
@ WARNING
Definition: logger.h:47
@ INFO
Definition: logger.h:46
Definition: session.h:86
QVector< QByteArray > splitToViews(const QByteArray &in, const QByteArray &sep, const Qt::SplitBehavior behavior=Qt::KeepEmptyParts)
Definition: bytearray.cpp:34
PythonInfo pythonInfo()
QString fileName(const QString &filePath)
Definition: fs.cpp:87
void removeDirRecursive(const QString &path)
Definition: fs.cpp:187
QString toUniformPath(const QString &path)
Definition: fs.cpp:69
QString expandPathAbs(const QString &path)
Definition: fs.cpp:309
bool forceRemove(const QString &filePath)
Definition: fs.cpp:173
QString toNativePath(const QString &path)
Definition: fs.cpp:64
nonstd::expected< void, QString > saveToFile(const QString &path, const QByteArray &data)
Definition: io.cpp:69
QString specialFolderLocation(const SpecialFolder folder)
Definition: profile.cpp:131
file(GLOB QBT_TS_FILES "${qBittorrent_SOURCE_DIR}/src/lang/*.ts") set_source_files_properties($
Definition: CMakeLists.txt:5
DownloadStatus status
QStringList supportedCategories
PluginVersion version