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"
35 #include <QDirIterator>
36 #include <QDomDocument>
37 #include <QDomElement>
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"
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
);
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
96 connect(Net::ProxyConfigurationManager::instance(), &Net::ProxyConfigurationManager::proxyConfigurationChanged
97 , this, &SearchPluginManager::applyProxySettings
);
98 connect(Preferences::instance(), &Preferences::changed
99 , this, &SearchPluginManager::applyProxySettings
);
100 applyProxySettings();
106 SearchPluginManager::~SearchPluginManager()
108 qDeleteAll(m_plugins
);
111 SearchPluginManager
*SearchPluginManager::instance()
114 m_instance
= new SearchPluginManager
;
118 void SearchPluginManager::freeInstance()
123 QStringList
SearchPluginManager::allPlugins() const
125 return m_plugins
.keys();
128 QStringList
SearchPluginManager::enabledPlugins() const
131 for (const PluginInfo
*plugin
: asConst(m_plugins
))
134 plugins
<< plugin
->name
;
140 QStringList
SearchPluginManager::supportedCategories() const
143 for (const PluginInfo
*plugin
: asConst(m_plugins
))
147 for (const QString
&cat
: plugin
->supportedCategories
)
149 if (!result
.contains(cat
))
158 QStringList
SearchPluginManager::getPluginCategories(const QString
&pluginName
) const
161 if (pluginName
== u
"all")
162 plugins
= allPlugins();
163 else if ((pluginName
== u
"enabled") || (pluginName
== u
"multi"))
164 plugins
= enabledPlugins();
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);
190 plugin
->enabled
= enabled
;
192 Preferences
*const pref
= Preferences::instance();
193 QStringList disabledPlugins
= pref
->getSearchEngDisabled();
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
))
218 DownloadManager::instance()->download(DownloadRequest(source
).saveToFile(true)
219 , Preferences::instance()->useProxyForGeneralPurposes()
220 , this, &SearchPluginManager::pluginDownloadFinished
);
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
);
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."));
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
);
262 Utils::Fs::copyFile(path
, destPath
);
263 // Update supported plugins
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
);
274 Utils::Fs::copyFile(backupPath
, destPath
);
275 Utils::Fs::removeFile(backupPath
);
276 // Update supported plugins
278 emit
pluginUpdateFailed(name
, tr("Plugin is not supported."));
282 emit
pluginInstallationFailed(name
, tr("Plugin is not supported."));
287 // Install was successful, remove backup
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
);
313 void SearchPluginManager::updateIconPath(PluginInfo
*const plugin
)
317 const Path pluginsPath
= pluginsLocation();
318 Path iconPath
= pluginsPath
/ Path(plugin
->name
+ u
".png");
319 if (iconPath
.exists())
321 plugin
->iconPath
= iconPath
;
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
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
);
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
));
411 proxyStrHTTP
= u
"http://%1:%2"_s
.arg(proxyConfig
.ip
, QString::number(proxyConfig
.port
));
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
));
423 proxyStrSOCK
= u
"%1:%2"_s
.arg(proxyConfig
.ip
, QString::number(proxyConfig
.port
));
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
);
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
);
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
));
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
);
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
)))
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()
510 nova
.setProcessEnvironment(QProcessEnvironment::systemEnvironment());
512 const QStringList params
514 Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG
,
515 (engineLocation() / Path(u
"/nova2.py"_s
)).toString(),
518 nova
.start(Utils::ForeignApps::pythonInfo().executableName
, params
, QIODevice::ReadOnly
);
519 nova
.waitForFinished();
521 const auto capabilities
= QString::fromUtf8(nova
.readAllStandardOutput());
523 if (!xmlDoc
.setContent(capabilities
))
525 qWarning() << "Could not parse Nova search engine capabilities, msg: " << capabilities
.toLocal8Bit().data();
526 qWarning() << "Error: " << nova
.readAllStandardError().constData();
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();
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
)
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;
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())));
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
))
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())
649 LogMsg(tr("Search plugin '%1' contains invalid version string ('%2')")
650 .arg(filePath
.filename(), versionStr
), Log::MsgType::WARNING
);