/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2018 Thomas Piccirello * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give permission to * link this program with the OpenSSL project's "OpenSSL" library (or with * modified versions of it that use the same license as the "OpenSSL" library), * and distribute the linked executables. You must obey the GNU General Public * License in all respects for all of the code used other than "OpenSSL". If you * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. */ #include "searchcontroller.h" #include #include #include #include #include "base/global.h" #include "base/logger.h" #include "base/search/searchhandler.h" #include "base/utils/foreignapps.h" #include "base/utils/random.h" #include "base/utils/string.h" #include "apierror.h" #include "isessionmanager.h" class SearchPluginManager; using SearchHandlerPtr = QSharedPointer; using SearchHandlerDict = QMap; namespace { const QLatin1String ACTIVE_SEARCHES("activeSearches"); const QLatin1String SEARCH_HANDLERS("searchHandlers"); void removeActiveSearch(ISession *session, const int id) { auto activeSearches = session->getData>(ACTIVE_SEARCHES); if (activeSearches.remove(id)) session->setData(ACTIVE_SEARCHES, QVariant::fromValue(activeSearches)); } } void SearchController::startAction() { requireParams({"pattern", "category", "plugins"}); if (!Utils::ForeignApps::pythonInfo().isValid()) throw APIError(APIErrorType::Conflict, tr("Python must be installed to use the Search Engine.")); const QString pattern = params()["pattern"].trimmed(); const QString category = params()["category"].trimmed(); const QStringList plugins = params()["plugins"].split('|'); QStringList pluginsToUse; if (plugins.size() == 1) { const QString pluginsLower = plugins[0].toLower(); if (pluginsLower == "all") pluginsToUse = SearchPluginManager::instance()->allPlugins(); else if ((pluginsLower == "enabled") || (pluginsLower == "multi")) pluginsToUse = SearchPluginManager::instance()->enabledPlugins(); else pluginsToUse << plugins; } else { pluginsToUse << plugins; } ISession *const session = sessionManager()->session(); auto activeSearches = session->getData>(ACTIVE_SEARCHES); if (activeSearches.size() >= MAX_CONCURRENT_SEARCHES) throw APIError(APIErrorType::Conflict, tr("Unable to create more than %1 concurrent searches.").arg(MAX_CONCURRENT_SEARCHES)); const auto id = generateSearchId(); const SearchHandlerPtr searchHandler {SearchPluginManager::instance()->startSearch(pattern, category, pluginsToUse)}; QObject::connect(searchHandler.data(), &SearchHandler::searchFinished, this, [session, id, this]() { searchFinished(session, id); }); QObject::connect(searchHandler.data(), &SearchHandler::searchFailed, this, [session, id, this]() { searchFailed(session, id); }); auto searchHandlers = session->getData(SEARCH_HANDLERS); searchHandlers.insert(id, searchHandler); session->setData(SEARCH_HANDLERS, QVariant::fromValue(searchHandlers)); activeSearches.insert(id); session->setData(ACTIVE_SEARCHES, QVariant::fromValue(activeSearches)); const QJsonObject result = {{"id", id}}; setResult(result); } void SearchController::stopAction() { requireParams({"id"}); const int id = params()["id"].toInt(); ISession *const session = sessionManager()->session(); const auto searchHandlers = session->getData(SEARCH_HANDLERS); if (!searchHandlers.contains(id)) throw APIError(APIErrorType::NotFound); const SearchHandlerPtr searchHandler = searchHandlers[id]; if (searchHandler->isActive()) { searchHandler->cancelSearch(); removeActiveSearch(session, id); } } void SearchController::statusAction() { const int id = params()["id"].toInt(); const auto searchHandlers = sessionManager()->session()->getData(SEARCH_HANDLERS); if ((id != 0) && !searchHandlers.contains(id)) throw APIError(APIErrorType::NotFound); QJsonArray statusArray; const QList searchIds {(id == 0) ? searchHandlers.keys() : QList {id}}; for (const int searchId : searchIds) { const SearchHandlerPtr searchHandler = searchHandlers[searchId]; statusArray << QJsonObject { {"id", searchId}, {"status", searchHandler->isActive() ? "Running" : "Stopped"}, {"total", searchHandler->results().size()} }; } setResult(statusArray); } void SearchController::resultsAction() { requireParams({"id"}); const int id = params()["id"].toInt(); int limit = params()["limit"].toInt(); int offset = params()["offset"].toInt(); const auto searchHandlers = sessionManager()->session()->getData(SEARCH_HANDLERS); if (!searchHandlers.contains(id)) throw APIError(APIErrorType::NotFound); const SearchHandlerPtr searchHandler = searchHandlers[id]; const QList searchResults = searchHandler->results(); const int size = searchResults.size(); if (offset > size) throw APIError(APIErrorType::Conflict, tr("Offset is out of range")); // normalize values if (offset < 0) offset = size + offset; if (offset < 0) // check again throw APIError(APIErrorType::Conflict, tr("Offset is out of range")); if (limit <= 0) limit = -1; if ((limit > 0) || (offset > 0)) setResult(getResults(searchResults.mid(offset, limit), searchHandler->isActive(), size)); else setResult(getResults(searchResults, searchHandler->isActive(), size)); } void SearchController::deleteAction() { requireParams({"id"}); const int id = params()["id"].toInt(); ISession *const session = sessionManager()->session(); auto searchHandlers = session->getData(SEARCH_HANDLERS); if (!searchHandlers.contains(id)) throw APIError(APIErrorType::NotFound); const SearchHandlerPtr searchHandler = searchHandlers[id]; searchHandler->cancelSearch(); searchHandlers.remove(id); session->setData(SEARCH_HANDLERS, QVariant::fromValue(searchHandlers)); removeActiveSearch(session, id); } void SearchController::categoriesAction() { QStringList categories; const QString name = params()["pluginName"].trimmed(); categories << SearchPluginManager::categoryFullName("all"); for (const QString &category : asConst(SearchPluginManager::instance()->getPluginCategories(name))) categories << SearchPluginManager::categoryFullName(category); const QJsonArray result = QJsonArray::fromStringList(categories); setResult(result); } void SearchController::pluginsAction() { const QStringList allPlugins = SearchPluginManager::instance()->allPlugins(); setResult(getPluginsInfo(allPlugins)); } void SearchController::installPluginAction() { requireParams({"sources"}); const QStringList sources = params()["sources"].split('|'); for (const QString &source : sources) SearchPluginManager::instance()->installPlugin(source); } void SearchController::uninstallPluginAction() { requireParams({"names"}); const QStringList names = params()["names"].split('|'); for (const QString &name : names) SearchPluginManager::instance()->uninstallPlugin(name.trimmed()); } void SearchController::enablePluginAction() { requireParams({"names", "enable"}); const QStringList names = params()["names"].split('|'); const bool enable = Utils::String::parseBool(params()["enable"].trimmed(), false); for (const QString &name : names) SearchPluginManager::instance()->enablePlugin(name.trimmed(), enable); } void SearchController::updatePluginsAction() { SearchPluginManager *const pluginManager = SearchPluginManager::instance(); connect(pluginManager, &SearchPluginManager::checkForUpdatesFinished, this, &SearchController::checkForUpdatesFinished); connect(pluginManager, &SearchPluginManager::checkForUpdatesFailed, this, &SearchController::checkForUpdatesFailed); pluginManager->checkForUpdates(); } void SearchController::checkForUpdatesFinished(const QHash &updateInfo) { if (updateInfo.isEmpty()) { LogMsg(tr("All plugins are already up to date."), Log::INFO); return; } LogMsg(tr("Updating %1 plugins").arg(updateInfo.size()), Log::INFO); SearchPluginManager *const pluginManager = SearchPluginManager::instance(); for (const QString &pluginName : asConst(updateInfo.keys())) { LogMsg(tr("Updating plugin %1").arg(pluginName), Log::INFO); pluginManager->updatePlugin(pluginName); } } void SearchController::checkForUpdatesFailed(const QString &reason) { LogMsg(tr("Failed to check for plugin updates: %1").arg(reason), Log::INFO); } void SearchController::searchFinished(ISession *session, const int id) { removeActiveSearch(session, id); } void SearchController::searchFailed(ISession *session, const int id) { removeActiveSearch(session, id); } int SearchController::generateSearchId() const { const auto searchHandlers = sessionManager()->session()->getData(SEARCH_HANDLERS); while (true) { const int id = Utils::Random::rand(1, std::numeric_limits::max()); if (!searchHandlers.contains(id)) return id; } } /** * Returns the search results in JSON format. * * The return value is an object with a status and an array of dictionaries. * The dictionary keys are: * - "fileName" * - "fileUrl" * - "fileSize" * - "nbSeeders" * - "nbLeechers" * - "siteUrl" * - "descrLink" */ QJsonObject SearchController::getResults(const QList &searchResults, const bool isSearchActive, const int totalResults) const { QJsonArray searchResultsArray; for (const SearchResult &searchResult : searchResults) { searchResultsArray << QJsonObject { {"fileName", searchResult.fileName}, {"fileUrl", searchResult.fileUrl}, {"fileSize", searchResult.fileSize}, {"nbSeeders", searchResult.nbSeeders}, {"nbLeechers", searchResult.nbLeechers}, {"siteUrl", searchResult.siteUrl}, {"descrLink", searchResult.descrLink} }; } const QJsonObject result = { {"status", isSearchActive ? "Running" : "Stopped"}, {"results", searchResultsArray}, {"total", totalResults} }; return result; } /** * Returns the search plugins in JSON format. * * The return value is an array of dictionaries. * The dictionary keys are: * - "name" * - "version" * - "fullName" * - "url" * - "supportedCategories" * - "iconPath" * - "enabled" */ QJsonArray SearchController::getPluginsInfo(const QStringList &plugins) const { QJsonArray pluginsArray; for (const QString &plugin : plugins) { const PluginInfo *const pluginInfo = SearchPluginManager::instance()->pluginInfo(plugin); pluginsArray << QJsonObject { {"name", pluginInfo->name}, {"version", QString(pluginInfo->version)}, {"fullName", pluginInfo->fullName}, {"url", pluginInfo->url}, {"supportedCategories", QJsonArray::fromStringList(pluginInfo->supportedCategories)}, {"enabled", pluginInfo->enabled} }; } return pluginsArray; }