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>
42 #include "base/global.h"
43 #include "base/logger.h"
44 #include "base/net/downloadmanager.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"
55 void clearPythonCache(const Path
&path
)
57 // remove python cache artifacts in `path` and subdirs
59 PathList dirs
= {path
};
60 QDirIterator iter
{path
.data(), (QDir::AllDirs
| QDir::NoDotAndDotDot
), QDirIterator::Subdirectories
};
61 while (iter
.hasNext())
62 dirs
+= Path(iter
.next());
64 for (const Path
&dir
: asConst(dirs
))
66 // python 3: remove "__pycache__" folders
67 if (dir
.filename() == QLatin1String("__pycache__"))
69 Utils::Fs::removeDirRecursively(dir
);
73 // python 2: remove "*.pyc" files
74 const QStringList files
= QDir(dir
.data()).entryList(QDir::Files
);
75 for (const QString
&file
: files
)
77 const Path path
{file
};
78 if (path
.hasExtension(QLatin1String(".pyc")))
79 Utils::Fs::removeFile(path
);
85 QPointer
<SearchPluginManager
> SearchPluginManager::m_instance
= nullptr;
87 SearchPluginManager::SearchPluginManager()
88 : m_updateUrl(QLatin1String("http://searchplugins.qbittorrent.org/nova3/engines/"))
90 Q_ASSERT(!m_instance
); // only one instance is allowed
97 SearchPluginManager::~SearchPluginManager()
99 qDeleteAll(m_plugins
);
102 SearchPluginManager
*SearchPluginManager::instance()
105 m_instance
= new SearchPluginManager
;
109 void SearchPluginManager::freeInstance()
114 QStringList
SearchPluginManager::allPlugins() const
116 return m_plugins
.keys();
119 QStringList
SearchPluginManager::enabledPlugins() const
122 for (const PluginInfo
*plugin
: asConst(m_plugins
))
125 plugins
<< plugin
->name
;
131 QStringList
SearchPluginManager::supportedCategories() const
134 for (const PluginInfo
*plugin
: asConst(m_plugins
))
138 for (const QString
&cat
: plugin
->supportedCategories
)
140 if (!result
.contains(cat
))
149 QStringList
SearchPluginManager::getPluginCategories(const QString
&pluginName
) const
152 if (pluginName
== u
"all")
153 plugins
= allPlugins();
154 else if ((pluginName
== u
"enabled") || (pluginName
== u
"multi"))
155 plugins
= enabledPlugins();
157 plugins
<< pluginName
.trimmed();
159 QSet
<QString
> categories
;
160 for (const QString
&name
: asConst(plugins
))
162 const PluginInfo
*plugin
= pluginInfo(name
);
163 if (!plugin
) continue; // plugin wasn't found
164 for (const QString
&category
: plugin
->supportedCategories
)
165 categories
<< category
;
168 return categories
.values();
171 PluginInfo
*SearchPluginManager::pluginInfo(const QString
&name
) const
173 return m_plugins
.value(name
);
176 void SearchPluginManager::enablePlugin(const QString
&name
, const bool enabled
)
178 PluginInfo
*plugin
= m_plugins
.value(name
, nullptr);
181 plugin
->enabled
= enabled
;
183 Preferences
*const pref
= Preferences::instance();
184 QStringList disabledPlugins
= pref
->getSearchEngDisabled();
186 disabledPlugins
.removeAll(name
);
187 else if (!disabledPlugins
.contains(name
))
188 disabledPlugins
.append(name
);
189 pref
->setSearchEngDisabled(disabledPlugins
);
191 emit
pluginEnabled(name
, enabled
);
195 // Updates shipped plugin
196 void SearchPluginManager::updatePlugin(const QString
&name
)
198 installPlugin(QString::fromLatin1("%1%2.py").arg(m_updateUrl
, name
));
201 // Install or update plugin from file or url
202 void SearchPluginManager::installPlugin(const QString
&source
)
204 clearPythonCache(engineLocation());
206 if (Net::DownloadManager::hasSupportedScheme(source
))
209 DownloadManager::instance()->download(DownloadRequest(source
).saveToFile(true)
210 , this, &SearchPluginManager::pluginDownloadFinished
);
214 const Path path
{source
.startsWith(u
"file:", Qt::CaseInsensitive
) ? QUrl(source
).toLocalFile() : source
};
216 QString pluginName
= path
.filename();
217 if (pluginName
.endsWith(u
".py", Qt::CaseInsensitive
))
219 pluginName
.chop(pluginName
.size() - pluginName
.lastIndexOf(u
'.'));
220 installPlugin_impl(pluginName
, path
);
224 emit
pluginInstallationFailed(pluginName
, tr("Unknown search engine plugin file format."));
229 void SearchPluginManager::installPlugin_impl(const QString
&name
, const Path
&path
)
231 const PluginVersion newVersion
= getPluginVersion(path
);
232 const PluginInfo
*plugin
= pluginInfo(name
);
233 if (plugin
&& !(plugin
->version
< newVersion
))
235 LogMsg(tr("Plugin already at version %1, which is greater than %2").arg(plugin
->version
, newVersion
), Log::INFO
);
236 emit
pluginUpdateFailed(name
, tr("A more recent version of this plugin is already installed."));
240 // Process with install
241 const Path destPath
= pluginPath(name
);
242 const Path backupPath
= destPath
+ ".bak";
243 bool updated
= false;
244 if (destPath
.exists())
246 // Backup in case install fails
247 Utils::Fs::copyFile(destPath
, backupPath
);
248 Utils::Fs::removeFile(destPath
);
252 Utils::Fs::copyFile(path
, destPath
);
253 // Update supported plugins
255 // Check if this was correctly installed
256 if (!m_plugins
.contains(name
))
258 // Remove broken file
259 Utils::Fs::removeFile(destPath
);
260 LogMsg(tr("Plugin %1 is not supported.").arg(name
), Log::INFO
);
264 Utils::Fs::copyFile(backupPath
, destPath
);
265 Utils::Fs::removeFile(backupPath
);
266 // Update supported plugins
268 emit
pluginUpdateFailed(name
, tr("Plugin is not supported."));
272 emit
pluginInstallationFailed(name
, tr("Plugin is not supported."));
277 // Install was successful, remove backup
280 LogMsg(tr("Plugin %1 has been successfully updated.").arg(name
), Log::INFO
);
281 Utils::Fs::removeFile(backupPath
);
286 bool SearchPluginManager::uninstallPlugin(const QString
&name
)
288 clearPythonCache(engineLocation());
290 // remove it from hard drive
291 const Path pluginsPath
= pluginsLocation();
292 const QStringList filters
{name
+ QLatin1String(".*")};
293 const QStringList files
= QDir(pluginsPath
.data()).entryList(filters
, QDir::Files
, QDir::Unsorted
);
294 for (const QString
&file
: files
)
295 Utils::Fs::removeFile(pluginsPath
/ Path(file
));
296 // Remove it from supported engines
297 delete m_plugins
.take(name
);
299 emit
pluginUninstalled(name
);
303 void SearchPluginManager::updateIconPath(PluginInfo
*const plugin
)
307 const Path pluginsPath
= pluginsLocation();
308 Path iconPath
= pluginsPath
/ Path(plugin
->name
+ QLatin1String(".png"));
309 if (iconPath
.exists())
311 plugin
->iconPath
= iconPath
;
315 iconPath
= pluginsPath
/ Path(plugin
->name
+ QLatin1String(".ico"));
316 if (iconPath
.exists())
317 plugin
->iconPath
= iconPath
;
321 void SearchPluginManager::checkForUpdates()
323 // Download version file from update server
325 DownloadManager::instance()->download({m_updateUrl
+ u
"versions.txt"}
326 , this, &SearchPluginManager::versionInfoDownloadFinished
);
329 SearchDownloadHandler
*SearchPluginManager::downloadTorrent(const QString
&siteUrl
, const QString
&url
)
331 return new SearchDownloadHandler
{siteUrl
, url
, this};
334 SearchHandler
*SearchPluginManager::startSearch(const QString
&pattern
, const QString
&category
, const QStringList
&usedPlugins
)
336 // No search pattern entered
337 Q_ASSERT(!pattern
.isEmpty());
339 return new SearchHandler
{pattern
, category
, usedPlugins
, this};
342 QString
SearchPluginManager::categoryFullName(const QString
&categoryName
)
344 const QHash
<QString
, QString
> categoryTable
346 {u
"all"_qs
, tr("All categories")},
347 {u
"movies"_qs
, tr("Movies")},
348 {u
"tv"_qs
, tr("TV shows")},
349 {u
"music"_qs
, tr("Music")},
350 {u
"games"_qs
, tr("Games")},
351 {u
"anime"_qs
, tr("Anime")},
352 {u
"software"_qs
, tr("Software")},
353 {u
"pictures"_qs
, tr("Pictures")},
354 {u
"books"_qs
, tr("Books")}
356 return categoryTable
.value(categoryName
);
359 QString
SearchPluginManager::pluginFullName(const QString
&pluginName
)
361 return pluginInfo(pluginName
) ? pluginInfo(pluginName
)->fullName
: QString();
364 Path
SearchPluginManager::pluginsLocation()
366 return (engineLocation() / Path(u
"engines"_qs
));
369 Path
SearchPluginManager::engineLocation()
371 static Path location
;
372 if (location
.isEmpty())
374 location
= specialFolderLocation(SpecialFolder::Data
) / Path(u
"nova3"_qs
);
375 Utils::Fs::mkpath(location
);
381 void SearchPluginManager::versionInfoDownloadFinished(const Net::DownloadResult
&result
)
383 if (result
.status
== Net::DownloadStatus::Success
)
384 parseVersionInfo(result
.data
);
386 emit
checkForUpdatesFailed(tr("Update server is temporarily unavailable. %1").arg(result
.errorString
));
389 void SearchPluginManager::pluginDownloadFinished(const Net::DownloadResult
&result
)
391 if (result
.status
== Net::DownloadStatus::Success
)
393 const Path filePath
= result
.filePath
;
395 Path pluginPath
{QUrl(result
.url
).path()};
396 pluginPath
.removeExtension(); // Remove extension
397 installPlugin_impl(pluginPath
.filename(), filePath
);
398 Utils::Fs::removeFile(filePath
);
402 const QString url
= result
.url
;
403 QString pluginName
= url
.mid(url
.lastIndexOf(u
'/') + 1);
404 pluginName
.replace(u
".py"_qs
, u
""_qs
, Qt::CaseInsensitive
);
406 if (pluginInfo(pluginName
))
407 emit
pluginUpdateFailed(pluginName
, tr("Failed to download the plugin file. %1").arg(result
.errorString
));
409 emit
pluginInstallationFailed(pluginName
, tr("Failed to download the plugin file. %1").arg(result
.errorString
));
413 // Update nova.py search plugin if necessary
414 void SearchPluginManager::updateNova()
416 // create nova directory if necessary
417 const Path enginePath
= engineLocation();
419 QFile packageFile
{(enginePath
/ Path(u
"__init__.py"_qs
)).data()};
420 packageFile
.open(QIODevice::WriteOnly
);
423 Utils::Fs::mkdir(enginePath
/ Path(u
"engines"_qs
));
425 QFile packageFile2
{(enginePath
/ Path(u
"engines/__init__.py"_qs
)).data()};
426 packageFile2
.open(QIODevice::WriteOnly
);
427 packageFile2
.close();
429 // Copy search plugin files (if necessary)
430 const auto updateFile
= [&enginePath
](const Path
&filename
, const bool compareVersion
)
432 const Path filePathBundled
= Path(u
":/searchengine/nova3"_qs
) / filename
;
433 const Path filePathDisk
= enginePath
/ filename
;
435 if (compareVersion
&& (getPluginVersion(filePathBundled
) <= getPluginVersion(filePathDisk
)))
438 Utils::Fs::removeFile(filePathDisk
);
439 Utils::Fs::copyFile(filePathBundled
, filePathDisk
);
442 updateFile(Path(u
"helpers.py"_qs
), true);
443 updateFile(Path(u
"nova2.py"_qs
), true);
444 updateFile(Path(u
"nova2dl.py"_qs
), true);
445 updateFile(Path(u
"novaprinter.py"_qs
), true);
446 updateFile(Path(u
"sgmllib3.py"_qs
), false);
447 updateFile(Path(u
"socks.py"_qs
), false);
450 void SearchPluginManager::update()
453 nova
.setProcessEnvironment(QProcessEnvironment::systemEnvironment());
455 const QStringList params
{(engineLocation() / Path(u
"/nova2.py"_qs
)).toString(), QLatin1String("--capabilities")};
456 nova
.start(Utils::ForeignApps::pythonInfo().executableName
, params
, QIODevice::ReadOnly
);
457 nova
.waitForFinished();
459 const auto capabilities
= QString::fromUtf8(nova
.readAll());
461 if (!xmlDoc
.setContent(capabilities
))
463 qWarning() << "Could not parse Nova search engine capabilities, msg: " << capabilities
.toLocal8Bit().data();
464 qWarning() << "Error: " << nova
.readAllStandardError().constData();
468 const QDomElement root
= xmlDoc
.documentElement();
469 if (root
.tagName() != u
"capabilities")
471 qWarning() << "Invalid XML file for Nova search engine capabilities, msg: " << capabilities
.toLocal8Bit().data();
475 for (QDomNode engineNode
= root
.firstChild(); !engineNode
.isNull(); engineNode
= engineNode
.nextSibling())
477 const QDomElement engineElem
= engineNode
.toElement();
478 if (!engineElem
.isNull())
480 const QString pluginName
= engineElem
.tagName();
482 auto plugin
= std::make_unique
<PluginInfo
>();
483 plugin
->name
= pluginName
;
484 plugin
->version
= getPluginVersion(pluginPath(pluginName
));
485 plugin
->fullName
= engineElem
.elementsByTagName(u
"name"_qs
).at(0).toElement().text();
486 plugin
->url
= engineElem
.elementsByTagName(u
"url"_qs
).at(0).toElement().text();
488 const QStringList categories
= engineElem
.elementsByTagName(u
"categories"_qs
).at(0).toElement().text().split(u
' ');
489 for (QString cat
: categories
)
493 plugin
->supportedCategories
<< cat
;
496 const QStringList disabledEngines
= Preferences::instance()->getSearchEngDisabled();
497 plugin
->enabled
= !disabledEngines
.contains(pluginName
);
499 updateIconPath(plugin
.get());
501 if (!m_plugins
.contains(pluginName
))
503 m_plugins
[pluginName
] = plugin
.release();
504 emit
pluginInstalled(pluginName
);
506 else if (m_plugins
[pluginName
]->version
!= plugin
->version
)
508 delete m_plugins
.take(pluginName
);
509 m_plugins
[pluginName
] = plugin
.release();
510 emit
pluginUpdated(pluginName
);
516 void SearchPluginManager::parseVersionInfo(const QByteArray
&info
)
518 QHash
<QString
, PluginVersion
> updateInfo
;
519 int numCorrectData
= 0;
521 const QVector
<QByteArray
> lines
= Utils::ByteArray::splitToViews(info
, "\n", Qt::SkipEmptyParts
);
522 for (QByteArray line
: lines
)
524 line
= line
.trimmed();
525 if (line
.isEmpty()) continue;
526 if (line
.startsWith('#')) continue;
528 const QVector
<QByteArray
> list
= Utils::ByteArray::splitToViews(line
, ":", Qt::SkipEmptyParts
);
529 if (list
.size() != 2) continue;
531 const auto pluginName
= QString::fromUtf8(list
.first().trimmed());
532 const PluginVersion version
= PluginVersion::tryParse(list
.last().trimmed(), {});
534 if (!version
.isValid()) continue;
537 if (isUpdateNeeded(pluginName
, version
))
539 LogMsg(tr("Plugin \"%1\" is outdated, updating to version %2").arg(pluginName
, version
), Log::INFO
);
540 updateInfo
[pluginName
] = version
;
544 if (numCorrectData
< lines
.size())
546 emit
checkForUpdatesFailed(tr("Incorrect update info received for %1 out of %2 plugins.")
547 .arg(QString::number(lines
.size() - numCorrectData
), QString::number(lines
.size())));
551 emit
checkForUpdatesFinished(updateInfo
);
555 bool SearchPluginManager::isUpdateNeeded(const QString
&pluginName
, const PluginVersion newVersion
) const
557 const PluginInfo
*plugin
= pluginInfo(pluginName
);
558 if (!plugin
) return true;
560 PluginVersion oldVersion
= plugin
->version
;
561 return (newVersion
> oldVersion
);
564 Path
SearchPluginManager::pluginPath(const QString
&name
)
566 return (pluginsLocation() / Path(name
+ QLatin1String(".py")));
569 PluginVersion
SearchPluginManager::getPluginVersion(const Path
&filePath
)
571 QFile pluginFile
{filePath
.data()};
572 if (!pluginFile
.open(QIODevice::ReadOnly
| QIODevice::Text
))
575 while (!pluginFile
.atEnd())
577 const auto line
= QString::fromUtf8(pluginFile
.readLine()).remove(u
' ');
578 if (!line
.startsWith(u
"#VERSION:", Qt::CaseInsensitive
)) continue;
580 const QString versionStr
= line
.mid(9);
581 const PluginVersion version
= PluginVersion::tryParse(versionStr
, {});
582 if (version
.isValid())
585 LogMsg(tr("Search plugin '%1' contains invalid version string ('%2')")
586 .arg(filePath
.filename(), versionStr
), Log::MsgType::WARNING
);