/* * Copyright (C) 2011-2018 Team Kodi * This file is part of Kodi - https://kodi.tv * * SPDX-License-Identifier: GPL-2.0-or-later * See LICENSES/README.md for more information. */ #include "AddonInstaller.h" #include "FileItem.h" #include "FilesystemInstaller.h" #include "GUIPassword.h" #include "GUIUserMessages.h" // for callback #include "ServiceBroker.h" #include "URL.h" #include "Util.h" #include "addons/AddonManager.h" #include "addons/AddonRepos.h" #include "addons/Repository.h" #include "addons/RepositoryUpdater.h" #include "addons/addoninfo/AddonInfo.h" #include "addons/addoninfo/AddonType.h" #include "dialogs/GUIDialogExtendedProgressBar.h" #include "events/AddonManagementEvent.h" #include "events/EventLog.h" #include "events/NotificationEvent.h" #include "favourites/FavouritesService.h" #include "filesystem/Directory.h" #include "filesystem/File.h" #include "guilib/GUIComponent.h" #include "guilib/GUIWindowManager.h" // for callback #include "guilib/LocalizeStrings.h" #include "messaging/helpers/DialogHelper.h" #include "messaging/helpers/DialogOKHelper.h" #include "settings/AdvancedSettings.h" #include "settings/Settings.h" #include "settings/SettingsComponent.h" #include "utils/FileOperationJob.h" #include "utils/FileUtils.h" #include "utils/JobManager.h" #include "utils/StringUtils.h" #include "utils/URIUtils.h" #include "utils/Variant.h" #include "utils/XTimeUtils.h" #include "utils/log.h" #include #include using namespace XFILE; using namespace ADDON; using namespace KODI::MESSAGING; using namespace std::chrono_literals; using KODI::MESSAGING::HELPERS::DialogResponse; using KODI::UTILITY::TypedDigest; namespace { class CAddonInstallJob : public CFileOperationJob { public: CAddonInstallJob(const ADDON::AddonPtr& addon, const ADDON::RepositoryPtr& repo, AutoUpdateJob isAutoUpdate); bool DoWork() override; static constexpr const char* TYPE_DOWNLOAD = "DOWNLOAD"; static constexpr const char* TYPE_INSTALL = "INSTALL"; /*! * \brief Returns the current processing type in the installation job * * \return The current processing type as string, can be \ref TYPE_DOWNLOAD or * \ref TYPE_INSTALL */ const char* GetType() const override { return m_currentType; } /*! \brief Find the add-on and its repository for the given add-on ID * \param addonID ID of the add-on to find * \param[out] repo the repository to use * \param[out] addon Add-on with the given add-on ID * \return True if the add-on and its repository were found, false otherwise. */ static bool GetAddon(const std::string& addonID, ADDON::RepositoryPtr& repo, ADDON::AddonPtr& addon); void SetDependsInstall(DependencyJob dependsInstall) { m_dependsInstall = dependsInstall; } void SetAllowCheckForUpdates(AllowCheckForUpdates allowCheckForUpdates) { m_allowCheckForUpdates = allowCheckForUpdates; }; private: void OnPreInstall(); void OnPostInstall(); bool Install(const std::string& installFrom, const ADDON::RepositoryPtr& repo = ADDON::RepositoryPtr()); bool DownloadPackage(const std::string& path, const std::string& dest); bool DoFileOperation(FileAction action, CFileItemList& items, const std::string& file, bool useSameJob = true); /*! \brief Queue a notification for addon installation/update failure \param addonID - addon id \param fileName - filename which is shown in case the addon id is unknown \param message - error message to be displayed */ void ReportInstallError(const std::string& addonID, const std::string& fileName, const std::string& message = ""); ADDON::AddonPtr m_addon; ADDON::RepositoryPtr m_repo; bool m_isUpdate; AutoUpdateJob m_isAutoUpdate; DependencyJob m_dependsInstall = DependencyJob::CHOICE_NO; AllowCheckForUpdates m_allowCheckForUpdates = AllowCheckForUpdates::CHOICE_YES; const char* m_currentType = TYPE_DOWNLOAD; }; class CAddonUnInstallJob : public CFileOperationJob { public: CAddonUnInstallJob(const ADDON::AddonPtr& addon, bool removeData); bool DoWork() override; void SetRecurseOrphaned(RecurseOrphaned recurseOrphaned) { m_recurseOrphaned = recurseOrphaned; }; private: void ClearFavourites(); ADDON::AddonPtr m_addon; bool m_removeData; RecurseOrphaned m_recurseOrphaned = RecurseOrphaned::CHOICE_YES; }; } // unnamed namespace CAddonInstaller::CAddonInstaller() : m_idle(true) { } CAddonInstaller::~CAddonInstaller() = default; CAddonInstaller &CAddonInstaller::GetInstance() { static CAddonInstaller addonInstaller; return addonInstaller; } void CAddonInstaller::OnJobComplete(unsigned int jobID, bool success, CJob* job) { std::unique_lock lock(m_critSection); JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair& p) { return p.second.jobID == jobID; }); if (i != m_downloadJobs.end()) m_downloadJobs.erase(i); if (m_downloadJobs.empty()) m_idle.Set(); lock.unlock(); PrunePackageCache(); CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE); CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); } void CAddonInstaller::OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job) { std::unique_lock lock(m_critSection); JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair& p) { return p.second.jobID == jobID; }); if (i != m_downloadJobs.end()) { // update job progress i->second.progress = 100 / total * progress; i->second.downloadFinshed = std::string(job->GetType()) == CAddonInstallJob::TYPE_INSTALL; CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM); msg.SetStringParam(i->first); lock.unlock(); CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); } } bool CAddonInstaller::IsDownloading() const { std::unique_lock lock(m_critSection); return !m_downloadJobs.empty(); } void CAddonInstaller::GetInstallList(VECADDONS &addons) const { std::unique_lock lock(m_critSection); std::vector addonIDs; for (JobMap::const_iterator i = m_downloadJobs.begin(); i != m_downloadJobs.end(); ++i) { if (i->second.jobID) addonIDs.push_back(i->first); } lock.unlock(); auto& addonMgr = CServiceBroker::GetAddonMgr(); for (const auto& addonId : addonIDs) { AddonPtr addon; if (addonMgr.FindInstallableById(addonId, addon)) addons.emplace_back(std::move(addon)); } } bool CAddonInstaller::GetProgress(const std::string& addonID, unsigned int& percent, bool& downloadFinshed) const { std::unique_lock lock(m_critSection); JobMap::const_iterator i = m_downloadJobs.find(addonID); if (i != m_downloadJobs.end()) { percent = i->second.progress; downloadFinshed = i->second.downloadFinshed; return true; } return false; } bool CAddonInstaller::Cancel(const std::string &addonID) { std::unique_lock lock(m_critSection); JobMap::iterator i = m_downloadJobs.find(addonID); if (i != m_downloadJobs.end()) { CServiceBroker::GetJobManager()->CancelJob(i->second.jobID); m_downloadJobs.erase(i); if (m_downloadJobs.empty()) m_idle.Set(); return true; } return false; } bool CAddonInstaller::InstallModal(const std::string& addonID, ADDON::AddonPtr& addon, InstallModalPrompt promptForInstall) { if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER)) return false; // we assume that addons that are enabled don't get to this routine (i.e. that GetAddon() has been called) if (CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, OnlyEnabled::CHOICE_NO)) return false; // addon is installed but disabled, and the user has specifically activated something that needs // the addon - should we enable it? // check we have it available if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon)) return false; // if specified ask the user if he wants it installed if (promptForInstall == InstallModalPrompt::CHOICE_YES) { if (HELPERS::ShowYesNoDialogLines(CVariant{24076}, CVariant{24100}, CVariant{addon->Name()}, CVariant{24101}) != DialogResponse::CHOICE_YES) { return false; } } if (!InstallOrUpdate(addonID, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_YES)) return false; return CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, OnlyEnabled::CHOICE_YES); } bool CAddonInstaller::InstallOrUpdate(const std::string& addonID, BackgroundJob background, ModalJob modal) { AddonPtr addon; RepositoryPtr repo; if (!CAddonInstallJob::GetAddon(addonID, repo, addon)) return false; return DoInstall(addon, repo, background, modal, AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO, AllowCheckForUpdates::CHOICE_YES); } bool CAddonInstaller::InstallOrUpdateDependency(const ADDON::AddonPtr& dependsId, const ADDON::RepositoryPtr& repo) { return DoInstall(dependsId, repo, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_NO, AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_YES, AllowCheckForUpdates::CHOICE_YES); } bool CAddonInstaller::RemoveDependency(const std::shared_ptr& dependsId) const { const bool removeData = CDirectory::Exists(dependsId->Profile()); CAddonUnInstallJob removeDependencyJob(dependsId, removeData); removeDependencyJob.SetRecurseOrphaned(RecurseOrphaned::CHOICE_NO); return removeDependencyJob.DoWork(); } std::vector CAddonInstaller::RemoveOrphanedDepsRecursively() const { std::vector removedItems; auto toRemove = CServiceBroker::GetAddonMgr().GetOrphanedDependencies(); while (toRemove.size() > 0) { for (const auto& dep : toRemove) { if (RemoveDependency(dep)) { removedItems.emplace_back(dep->Name()); // successfully removed } else { CLog::Log(LOGERROR, "CAddonMgr::{}: failed to remove orphaned add-on/dependency: {}", __func__, dep->Name()); } } toRemove = CServiceBroker::GetAddonMgr().GetOrphanedDependencies(); } return removedItems; } bool CAddonInstaller::Install(const std::string& addonId, const CAddonVersion& version, const std::string& repoId) { CLog::Log(LOGDEBUG, "CAddonInstaller: installing '{}' version '{}' from repository '{}'", addonId, version.asString(), repoId); AddonPtr addon; CAddonDatabase database; if (!database.Open() || !database.GetAddon(addonId, version, repoId, addon)) return false; AddonPtr repo; if (!CServiceBroker::GetAddonMgr().GetAddon(repoId, repo, AddonType::REPOSITORY, OnlyEnabled::CHOICE_YES)) return false; return DoInstall(addon, std::static_pointer_cast(repo), BackgroundJob::CHOICE_YES, ModalJob::CHOICE_NO, AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO, AllowCheckForUpdates::CHOICE_YES); } bool CAddonInstaller::DoInstall(const AddonPtr& addon, const RepositoryPtr& repo, BackgroundJob background, ModalJob modal, AutoUpdateJob autoUpdate, DependencyJob dependsInstall, AllowCheckForUpdates allowCheckForUpdates) { // check whether we already have the addon installing std::unique_lock lock(m_critSection); if (m_downloadJobs.find(addon->ID()) != m_downloadJobs.end()) return false; CAddonInstallJob* installJob = new CAddonInstallJob(addon, repo, autoUpdate); if (background == BackgroundJob::CHOICE_YES) { // Workaround: because CAddonInstallJob is blocking waiting for other jobs, it needs to be run // with priority dedicated. unsigned int jobID = CServiceBroker::GetJobManager()->AddJob(installJob, this, CJob::PRIORITY_DEDICATED); m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(jobID))); m_idle.Reset(); return true; } m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(0))); m_idle.Reset(); lock.unlock(); installJob->SetDependsInstall(dependsInstall); installJob->SetAllowCheckForUpdates(allowCheckForUpdates); bool result = false; if (modal == ModalJob::CHOICE_YES) result = installJob->DoModal(); else result = installJob->DoWork(); delete installJob; lock.lock(); JobMap::iterator i = m_downloadJobs.find(addon->ID()); m_downloadJobs.erase(i); if (m_downloadJobs.empty()) m_idle.Set(); return result; } bool CAddonInstaller::InstallFromZip(const std::string &path) { if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER)) return false; CLog::Log(LOGDEBUG, "CAddonInstaller: installing from zip '{}'", CURL::GetRedacted(path)); // grab the descriptive XML document from the zip, and read it in CFileItemList items; //! @bug some zip files return a single item (root folder) that we think is stored, so we don't use the zip:// protocol CURL pathToUrl(path); CURL zipDir = URIUtils::CreateArchivePath("zip", pathToUrl, ""); auto eventLog = CServiceBroker::GetEventLog(); if (!CDirectory::GetDirectory(zipDir, items, "", DIR_FLAG_DEFAULTS) || items.Size() != 1 || !items[0]->m_bIsFolder) { if (eventLog) eventLog->AddWithNotification(EventPtr( new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings.Get(24143), path), "special://xbmc/media/icon256x256.png", EventLevel::Error))); CLog::Log( LOGERROR, "CAddonInstaller: installing addon failed '{}' - itemsize: {}, first item is folder: {}", CURL::GetRedacted(path), items.Size(), items[0]->m_bIsFolder); return false; } AddonPtr addon; if (CServiceBroker::GetAddonMgr().LoadAddonDescription(items[0]->GetPath(), addon)) return DoInstall(addon, RepositoryPtr(), BackgroundJob::CHOICE_YES, ModalJob::CHOICE_NO, AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO, AllowCheckForUpdates::CHOICE_YES); if (eventLog) eventLog->AddWithNotification(EventPtr( new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings.Get(24143), path), "special://xbmc/media/icon256x256.png", EventLevel::Error))); return false; } bool CAddonInstaller::UnInstall(const AddonPtr& addon, bool removeData) { CServiceBroker::GetJobManager()->AddJob(new CAddonUnInstallJob(addon, removeData), this); return true; } bool CAddonInstaller::CheckDependencies(const AddonPtr& addon, CAddonDatabase* database /* = nullptr */) { std::pair failedDep; return CheckDependencies(addon, failedDep, database); } bool CAddonInstaller::CheckDependencies(const AddonPtr& addon, std::pair& failedDep, CAddonDatabase* database /* = nullptr */) { std::vector preDeps; preDeps.push_back(addon->ID()); CAddonDatabase localDB; if (!database) database = &localDB; return CheckDependencies(addon, preDeps, *database, failedDep); } bool CAddonInstaller::CheckDependencies(const AddonPtr &addon, std::vector& preDeps, CAddonDatabase &database, std::pair &failedDep) { if (addon == nullptr) return true; // a nullptr addon has no dependencies for (const auto& it : addon->GetDependencies()) { const std::string &addonID = it.id; const CAddonVersion& versionMin = it.versionMin; const CAddonVersion& version = it.version; bool optional = it.optional; AddonPtr dep; const bool haveInstalledAddon = CServiceBroker::GetAddonMgr().GetAddon(addonID, dep, OnlyEnabled::CHOICE_NO); if ((haveInstalledAddon && !dep->MeetsVersion(versionMin, version)) || (!haveInstalledAddon && !optional)) { // we have it but our version isn't good enough, or we don't have it and we need it if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, dep) || (dep && !dep->MeetsVersion(versionMin, version))) { // we don't have it in a repo, or we have it but the version isn't good enough, so dep isn't satisfied. CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: requires {} version {} which is not available", addon->ID(), addonID, version.asString()); // fill in the details of the failed dependency failedDep.first = addonID; failedDep.second = version.asString(); return false; } } // need to enable the dependency if (dep && CServiceBroker::GetAddonMgr().IsAddonDisabled(addonID) && !CServiceBroker::GetAddonMgr().EnableAddon(addonID)) { return false; } // at this point we have our dep, or the dep is optional (and we don't have it) so check that it's OK as well //! @todo should we assume that installed deps are OK? if (dep && std::find(preDeps.begin(), preDeps.end(), dep->ID()) == preDeps.end()) { preDeps.push_back(dep->ID()); if (!CheckDependencies(dep, preDeps, database, failedDep)) { database.Close(); return false; } } } database.Close(); return true; } bool CAddonInstaller::HasJob(const std::string& ID) const { std::unique_lock lock(m_critSection); return m_downloadJobs.find(ID) != m_downloadJobs.end(); } void CAddonInstaller::PrunePackageCache() { std::map> packs; int64_t size = EnumeratePackageFolder(packs); int64_t limit = static_cast(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_addonPackageFolderSize) * 1024 * 1024; if (size < limit) return; // Prune packages // 1. Remove the largest packages, leaving at least 2 for each add-on CFileItemList items; CAddonDatabase db; db.Open(); for (auto it = packs.begin(); it != packs.end(); ++it) { it->second->Sort(SortByLabel, SortOrderDescending); for (int j = 2; j < it->second->Size(); j++) items.Add(CFileItemPtr(new CFileItem(*it->second->Get(j)))); } items.Sort(SortBySize, SortOrderDescending); int i = 0; while (size > limit && i < items.Size()) { size -= items[i]->m_dwSize; db.RemovePackage(items[i]->GetPath()); CFileUtils::DeleteItem(items[i++]); } if (size > limit) { // 2. Remove the oldest packages (leaving least 1 for each add-on) items.Clear(); for (auto it = packs.begin(); it != packs.end(); ++it) { if (it->second->Size() > 1) items.Add(CFileItemPtr(new CFileItem(*it->second->Get(1)))); } items.Sort(SortByDate, SortOrderAscending); i = 0; while (size > limit && i < items.Size()) { size -= items[i]->m_dwSize; db.RemovePackage(items[i]->GetPath()); CFileUtils::DeleteItem(items[i++]); } } } void CAddonInstaller::InstallAddons(const VECADDONS& addons, bool wait, AllowCheckForUpdates allowCheckForUpdates) { for (const auto& addon : addons) { AddonPtr toInstall; RepositoryPtr repo; if (CAddonInstallJob::GetAddon(addon->ID(), repo, toInstall)) DoInstall(toInstall, repo, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_NO, AutoUpdateJob::CHOICE_YES, DependencyJob::CHOICE_NO, allowCheckForUpdates); } if (wait) { std::unique_lock lock(m_critSection); if (!m_downloadJobs.empty()) { m_idle.Reset(); lock.unlock(); m_idle.Wait(); } } } int64_t CAddonInstaller::EnumeratePackageFolder( std::map>& result) { CFileItemList items; CDirectory::GetDirectory("special://home/addons/packages/",items,".zip",DIR_FLAG_NO_FILE_DIRS); int64_t size = 0; for (int i = 0; i < items.Size(); i++) { if (items[i]->m_bIsFolder) continue; size += items[i]->m_dwSize; std::string pack,dummy; CAddonVersion::SplitFileName(pack, dummy, items[i]->GetLabel()); if (result.find(pack) == result.end()) result[pack] = std::make_unique(); result[pack]->Add(CFileItemPtr(new CFileItem(*items[i]))); } return size; } CAddonInstallJob::CAddonInstallJob(const AddonPtr& addon, const RepositoryPtr& repo, AutoUpdateJob isAutoUpdate) : m_addon(addon), m_repo(repo), m_isAutoUpdate(isAutoUpdate) { AddonPtr dummy; m_isUpdate = CServiceBroker::GetAddonMgr().GetAddon(addon->ID(), dummy, OnlyEnabled::CHOICE_NO); } bool CAddonInstallJob::GetAddon(const std::string& addonID, RepositoryPtr& repo, ADDON::AddonPtr& addon) { if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon)) return false; AddonPtr tmp; if (!CServiceBroker::GetAddonMgr().GetAddon(addon->Origin(), tmp, AddonType::REPOSITORY, OnlyEnabled::CHOICE_YES)) return false; repo = std::static_pointer_cast(tmp); return true; } bool CAddonInstallJob::DoWork() { m_currentType = CAddonInstallJob::TYPE_DOWNLOAD; SetTitle(StringUtils::Format(g_localizeStrings.Get(24057), m_addon->Name())); SetProgress(0); // check whether all the dependencies are available or not SetText(g_localizeStrings.Get(24058)); std::pair failedDep; if (!CAddonInstaller::GetInstance().CheckDependencies(m_addon, failedDep)) { std::string details = StringUtils::Format(g_localizeStrings.Get(24142), failedDep.first, failedDep.second); CLog::Log(LOGERROR, "CAddonInstallJob[{}]: {}", m_addon->ID(), details); ReportInstallError(m_addon->ID(), m_addon->ID(), details); return false; } std::string installFrom; { // Addons are installed by downloading the .zip package on the server to the local // packages folder, then extracting from the local .zip package into the addons folder // Both these functions are achieved by "copying" using the vfs. if (!m_repo && URIUtils::HasSlashAtEnd(m_addon->Path())) { // passed in a folder - all we need do is copy it across installFrom = m_addon->Path(); } else { std::string path{m_addon->Path()}; TypedDigest hash; if (m_repo) { CRepository::ResolveResult resolvedAddon = m_repo->ResolvePathAndHash(m_addon); path = resolvedAddon.location; hash = resolvedAddon.digest; if (path.empty()) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to resolve addon install source path", m_addon->ID()); ReportInstallError(m_addon->ID(), m_addon->ID()); return false; } } CAddonDatabase db; if (!db.Open()) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to open database", m_addon->ID()); ReportInstallError(m_addon->ID(), m_addon->ID()); return false; } std::string packageOriginalPath, packageFileName; URIUtils::Split(path, packageOriginalPath, packageFileName); // Use ChangeBasePath so the URL is decoded if necessary const std::string packagePath = "special://home/addons/packages/"; //!@todo fix design flaw in file copying: We use CFileOperationJob to download the package from the internet // to the local cache. It tries to be "smart" and decode the URL. But it never tells us what the result is, // so if we try for example to download "http://localhost/a+b.zip" the result ends up in "a b.zip". // First bug is that it actually decodes "+", which is not necessary except in query parts. Second bug // is that we cannot know that it does this and what the result is so the package will not be found without // using ChangeBasePath here (which is the same function the copying code uses and performs the translation). std::string package = URIUtils::ChangeBasePath(packageOriginalPath, packageFileName, packagePath); // check that we don't already have a valid copy if (!hash.Empty()) { std::string hashExisting; if (db.GetPackageHash(m_addon->ID(), package, hashExisting) && hash.value != hashExisting) { db.RemovePackage(package); } if (CFile::Exists(package)) { CFile::Delete(package); } } // zip passed in - download + extract if (!CFile::Exists(package)) { if (!DownloadPackage(path, packagePath)) { CFile::Delete(package); CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to download {}", m_addon->ID(), package); ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package)); return false; } } // at this point we have the package - check that it is valid SetText(g_localizeStrings.Get(24077)); if (!hash.Empty()) { TypedDigest actualHash{hash.type, CUtil::GetFileDigest(package, hash.type)}; if (hash != actualHash) { CFile::Delete(package); CLog::Log(LOGERROR, "CAddonInstallJob[{}]: Hash mismatch after download. Expected {}, was {}", m_addon->ID(), hash.value, actualHash.value); ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package)); return false; } db.AddPackage(m_addon->ID(), package, hash.value); } // check if the archive is valid CURL archive = URIUtils::CreateArchivePath("zip", CURL(package), ""); CFileItemList archivedFiles; AddonPtr temp; if (!CDirectory::GetDirectory(archive, archivedFiles, "", DIR_FLAG_DEFAULTS) || archivedFiles.Size() != 1 || !archivedFiles[0]->m_bIsFolder || !CServiceBroker::GetAddonMgr().LoadAddonDescription(archivedFiles[0]->GetPath(), temp)) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: invalid package {}", m_addon->ID(), package); db.RemovePackage(package); CFile::Delete(package); ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package)); return false; } installFrom = package; } } m_currentType = CAddonInstallJob::TYPE_INSTALL; // run any pre-install functions ADDON::OnPreInstall(m_addon); if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID())) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to unload addon.", m_addon->ID()); return false; } // perform install if (!Install(installFrom, m_repo)) return false; // Load new installed and if successed replace defined m_addon here with new one if (!CServiceBroker::GetAddonMgr().LoadAddon(m_addon->ID(), m_addon->Origin(), m_addon->Version()) || !CServiceBroker::GetAddonMgr().GetAddon(m_addon->ID(), m_addon, OnlyEnabled::CHOICE_YES)) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to reload addon", m_addon->ID()); return false; } g_localizeStrings.LoadAddonStrings(URIUtils::AddFileToFolder(m_addon->Path(), "resources/language/"), CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE), m_addon->ID()); ADDON::OnPostInstall(m_addon, m_isUpdate, IsModal()); // Write origin to database via addon manager, where this information is up-to-date. // Needed to set origin correctly for new installed addons. std::string origin; if (m_addon->Origin() == ORIGIN_SYSTEM) { origin = ORIGIN_SYSTEM; // keep system add-on origin as ORIGIN_SYSTEM } else if (m_addon->HasMainType(AddonType::REPOSITORY)) { origin = m_addon->ID(); // use own id as origin if repository // if a repository is updated during the add-on migration process, we need to skip // calling CheckForUpdates() on the repo to prevent deadlock issues during migration if (m_allowCheckForUpdates == AllowCheckForUpdates::CHOICE_YES) { if (m_isUpdate) { CLog::Log(LOGDEBUG, "ADDONS: repository [{}] updated. now checking for content updates.", m_addon->ID()); CServiceBroker::GetRepositoryUpdater().CheckForUpdates( std::static_pointer_cast(m_addon), false); } } else { CLog::Log(LOGDEBUG, "ADDONS: skipping CheckForUpdates() on repository [{}].", m_addon->ID()); } } else if (m_repo) { origin = m_repo->ID(); // use repo id as origin } CServiceBroker::GetAddonMgr().SetAddonOrigin(m_addon->ID(), origin, m_isUpdate); if (m_dependsInstall == DependencyJob::CHOICE_YES) { CLog::Log(LOGDEBUG, "ADDONS: dependency [{}] will not be version checked and unpinned", m_addon->ID()); } else { // we only do pinning/unpinning for non-system add-ons if (m_addon->Origin() != ORIGIN_SYSTEM) { // get all compatible versions of an addon-id regardless of their origin // from all installed repositories std::vector> compatibleVersions = CServiceBroker::GetAddonMgr().GetCompatibleVersions(m_addon->ID()); if (!m_addon->Origin().empty()) { // handle add-ons that originate from a repository // find the latest version for the origin we installed from CAddonVersion latestVersion; for (const auto& compatibleVersion : compatibleVersions) { if (compatibleVersion->Origin() == m_addon->Origin() && compatibleVersion->Version() > latestVersion) { latestVersion = compatibleVersion->Version(); } } if (m_addon->Version() == latestVersion) { // unpin the installed addon if it's the latest of its origin CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(), AddonUpdateRule::PIN_OLD_VERSION); CLog::Log(LOGDEBUG, "ADDONS: unpinned Addon: [{}] Origin: [{}] Version: [{}]", m_addon->ID(), m_addon->Origin(), m_addon->Version().asString()); } else { // pin if it is not the latest CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(), AddonUpdateRule::PIN_OLD_VERSION); CLog::Log(LOGDEBUG, "ADDONS: pinned Addon: [{}] Origin: [{}] Version: [{}]", m_addon->ID(), m_addon->Origin(), m_addon->Version().asString()); } } else { // handle manually installed add-ons // find the latest version of any origin/repository CAddonVersion latestVersion; for (const auto& compatibleVersion : compatibleVersions) { if (compatibleVersion->Version() > latestVersion) { latestVersion = compatibleVersion->Version(); } } if (m_addon->Version() < latestVersion) { // pin zip version if it's lesser than latest from repo(s) CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(), AddonUpdateRule::PIN_ZIP_INSTALL); CLog::Log(LOGDEBUG, "ADDONS: pinned zip installed Addon: [{}] Version: [{}]", m_addon->ID(), m_addon->Version().asString()); } else { // unpin zip version if it's >= the latest from repos CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(), AddonUpdateRule::PIN_ZIP_INSTALL); CLog::Log(LOGDEBUG, "ADDONS: unpinned zip installed Addon: [{}] Version: [{}]", m_addon->ID(), m_addon->Version().asString()); } } } } bool notify = (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( CSettings::SETTING_ADDONS_NOTIFICATIONS) || m_isAutoUpdate == AutoUpdateJob::CHOICE_NO) && !IsModal() && m_dependsInstall == DependencyJob::CHOICE_NO; auto eventLog = CServiceBroker::GetEventLog(); if (eventLog) eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, m_isUpdate ? 24065 : 24084)), notify, false); if (m_isAutoUpdate == AutoUpdateJob::CHOICE_YES && m_addon->LifecycleState() == AddonLifecycleState::BROKEN) { CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: auto-disabling due to being marked as broken", m_addon->ID()); CServiceBroker::GetAddonMgr().DisableAddon(m_addon->ID(), AddonDisabledReason::USER); if (eventLog) eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, 24094)), true, false); } else if (m_addon->LifecycleState() == AddonLifecycleState::DEPRECATED) { CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: installed addon marked as deprecated", m_addon->ID()); std::string text = StringUtils::Format(g_localizeStrings.Get(24168), m_addon->LifecycleStateDescription()); if (eventLog) eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, text)), true, false); } // and we're done! MarkFinished(); return true; } bool CAddonInstallJob::DownloadPackage(const std::string &path, const std::string &dest) { if (ShouldCancel(0, 1)) return false; SetText(g_localizeStrings.Get(24078)); // need to download/copy the package first CFileItemList list; list.Add(CFileItemPtr(new CFileItem(path, false))); list[0]->Select(true); return DoFileOperation(CFileOperationJob::ActionReplace, list, dest, true); } bool CAddonInstallJob::DoFileOperation(FileAction action, CFileItemList &items, const std::string &file, bool useSameJob /* = true */) { bool result = false; if (useSameJob) { SetFileOperation(action, items, file); // temporarily disable auto-closing so not to close the current progress indicator bool autoClose = GetAutoClose(); if (autoClose) SetAutoClose(false); // temporarily disable updating title or text bool updateInformation = GetUpdateInformation(); if (updateInformation) SetUpdateInformation(false); result = CFileOperationJob::DoWork(); SetUpdateInformation(updateInformation); SetAutoClose(autoClose); } else { CFileOperationJob job(action, items, file); // pass our progress indicators to the temporary job and only allow it to // show progress updates (no title or text changes) job.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), GetUpdateProgress(), false); result = job.DoWork(); } return result; } bool CAddonInstallJob::Install(const std::string &installFrom, const RepositoryPtr& repo) { const auto& deps = m_addon->GetDependencies(); if (!deps.empty() && m_addon->HasType(AddonType::REPOSITORY)) { bool notSystemAddon = std::none_of(deps.begin(), deps.end(), [](const DependencyInfo& dep) { return CServiceBroker::GetAddonMgr().IsSystemAddon(dep.id); }); if (notSystemAddon) { CLog::Log( LOGERROR, "CAddonInstallJob::{}: failed to install repository [{}]. It has dependencies defined", __func__, m_addon->ID()); ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24088)); return false; } } SetText(g_localizeStrings.Get(24079)); unsigned int totalSteps = static_cast(deps.size()) + 1; if (ShouldCancel(0, totalSteps)) return false; CAddonRepos addonRepos; if (!addonRepos.IsValid()) return false; // The first thing we do is install dependencies for (auto it = deps.begin(); it != deps.end(); ++it) { if (it->id != "xbmc.metadata") { const std::string &addonID = it->id; const CAddonVersion& versionMin = it->versionMin; const CAddonVersion& version = it->version; bool optional = it->optional; AddonPtr dependency; const bool haveInstalledAddon = CServiceBroker::GetAddonMgr().GetAddon(addonID, dependency, OnlyEnabled::CHOICE_NO); if ((haveInstalledAddon && !dependency->MeetsVersion(versionMin, version)) || (!haveInstalledAddon && !optional)) { // we have it but our version isn't good enough, or we don't have it and we need it // dependency is already queued up for install - ::Install will fail // instead we wait until the Job has finished. note that we // recall install on purpose in case prior installation failed if (CAddonInstaller::GetInstance().HasJob(addonID)) { while (CAddonInstaller::GetInstance().HasJob(addonID)) KODI::TIME::Sleep(50ms); if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(addonID)) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}", m_addon->ID(), addonID); ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085)); return false; } } // don't have the addon or the addon isn't new enough - grab it (no new job for these) else { RepositoryPtr repoForDep; AddonPtr dependencyToInstall; // origin of m_addon is empty at least if an addon is installed for the first time // we need to override "parentRepoId" if the passed in repo is valid. const std::string& parentRepoId = m_addon->Origin().empty() && repo ? repo->ID() : m_addon->Origin(); if (!addonRepos.FindDependency(addonID, parentRepoId, dependencyToInstall, repoForDep)) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to find dependency {}", m_addon->ID(), addonID); ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085)); return false; } else { if (!dependencyToInstall->MeetsVersion(versionMin, version)) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: found dependency [{}/{}] doesn't meet minimum " "version [{}]", m_addon->ID(), addonID, dependencyToInstall->Version().asString(), versionMin.asString()); ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085)); return false; } } if (IsModal()) { CAddonInstallJob dependencyJob(dependencyToInstall, repoForDep, AutoUpdateJob::CHOICE_NO); dependencyJob.SetDependsInstall(DependencyJob::CHOICE_YES); // pass our progress indicators to the temporary job and don't allow it to // show progress or information updates (no progress, title or text changes) dependencyJob.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), false, false); if (!dependencyJob.DoModal()) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}", m_addon->ID(), addonID); ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085)); return false; } } else if (!CAddonInstaller::GetInstance().InstallOrUpdateDependency(dependencyToInstall, repoForDep)) { CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}", m_addon->ID(), dependencyToInstall->ID()); ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085)); return false; } } } } if (ShouldCancel(std::distance(deps.begin(), it), totalSteps)) return false; } SetText(g_localizeStrings.Get(24086)); SetProgress(static_cast(100.0 * (totalSteps - 1.0) / totalSteps)); CFilesystemInstaller fsInstaller; if (!fsInstaller.InstallToFilesystem(installFrom, m_addon->ID())) { ReportInstallError(m_addon->ID(), m_addon->ID()); return false; } SetProgress(100); return true; } void CAddonInstallJob::ReportInstallError(const std::string& addonID, const std::string& fileName, const std::string& message /* = "" */) { AddonPtr addon; CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon); MarkFinished(); std::string msg = message; EventPtr activity; if (addon != nullptr) { AddonPtr addon2; bool success = CServiceBroker::GetAddonMgr().GetAddon(addonID, addon2, OnlyEnabled::CHOICE_YES); if (msg.empty()) { msg = g_localizeStrings.Get(addon2 != nullptr && success ? 113 : 114); } activity = EventPtr(new CAddonManagementEvent(addon, EventLevel::Error, msg)); if (IsModal()) HELPERS::ShowOKDialogText(CVariant{m_addon->Name()}, CVariant{msg}); } else { activity = EventPtr(new CNotificationEvent( 24045, !msg.empty() ? msg : StringUtils::Format(g_localizeStrings.Get(24143), fileName), EventLevel::Error)); if (IsModal()) HELPERS::ShowOKDialogText(CVariant{fileName}, CVariant{msg}); } auto eventLog = CServiceBroker::GetEventLog(); if (eventLog) eventLog->Add(activity, !IsModal(), false); } CAddonUnInstallJob::CAddonUnInstallJob(const AddonPtr &addon, bool removeData) : m_addon(addon), m_removeData(removeData) { } bool CAddonUnInstallJob::DoWork() { ADDON::OnPreUnInstall(m_addon); //Unregister addon with the manager to ensure nothing tries //to interact with it while we are uninstalling. if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID())) { CLog::Log(LOGERROR, "CAddonUnInstallJob[{}]: failed to unload addon.", m_addon->ID()); return false; } CFilesystemInstaller fsInstaller; if (!fsInstaller.UnInstallFromFilesystem(m_addon->Path())) { CLog::Log(LOGERROR, "CAddonUnInstallJob[{}]: could not delete addon data.", m_addon->ID()); return false; } ClearFavourites(); if (m_removeData) { CFileUtils::DeleteItem(m_addon->Profile()); } AddonPtr addon; // try to get the addon object from the repository as the local one does not exist anymore // if that doesn't work fall back to the local one if (!CServiceBroker::GetAddonMgr().FindInstallableById(m_addon->ID(), addon) || addon == nullptr) { addon = m_addon; } auto eventLog = CServiceBroker::GetEventLog(); if (eventLog) eventLog->Add(EventPtr(new CAddonManagementEvent(addon, 24144))); // Add-on uninstalled CServiceBroker::GetAddonMgr().OnPostUnInstall(m_addon->ID()); CAddonDatabase database; if (database.Open()) database.OnPostUnInstall(m_addon->ID()); ADDON::OnPostUnInstall(m_addon); if (m_recurseOrphaned == RecurseOrphaned::CHOICE_YES) { const auto removedItems = CAddonInstaller::GetInstance().RemoveOrphanedDepsRecursively(); if (removedItems.size() > 0) { CLog::Log(LOGINFO, "CAddonUnInstallJob[{}]: removed orphaned dependencies ({})", m_addon->ID(), StringUtils::Join(removedItems, ", ")); } } return true; } void CAddonUnInstallJob::ClearFavourites() { bool bSave = false; CFileItemList items; CServiceBroker::GetFavouritesService().GetAll(items); for (int i = 0; i < items.Size(); i++) { if (items[i]->GetPath().find(m_addon->ID()) != std::string::npos) { items.Remove(items[i].get()); bSave = true; } } if (bSave) CServiceBroker::GetFavouritesService().Save(items); }