Merge pull request #24470 from fuzzard/release_20.3
[xbmc.git] / xbmc / addons / AddonInstaller.cpp
blob0a0d1694dda90d2226632aed3f96101f43e06632
1 /*
2 * Copyright (C) 2011-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
9 #include "AddonInstaller.h"
11 #include "FileItem.h"
12 #include "FilesystemInstaller.h"
13 #include "GUIPassword.h"
14 #include "GUIUserMessages.h" // for callback
15 #include "ServiceBroker.h"
16 #include "URL.h"
17 #include "Util.h"
18 #include "addons/AddonManager.h"
19 #include "addons/AddonRepos.h"
20 #include "addons/Repository.h"
21 #include "addons/RepositoryUpdater.h"
22 #include "addons/addoninfo/AddonInfo.h"
23 #include "addons/addoninfo/AddonType.h"
24 #include "dialogs/GUIDialogExtendedProgressBar.h"
25 #include "events/AddonManagementEvent.h"
26 #include "events/EventLog.h"
27 #include "events/NotificationEvent.h"
28 #include "favourites/FavouritesService.h"
29 #include "filesystem/Directory.h"
30 #include "filesystem/File.h"
31 #include "guilib/GUIComponent.h"
32 #include "guilib/GUIWindowManager.h" // for callback
33 #include "guilib/LocalizeStrings.h"
34 #include "messaging/helpers/DialogHelper.h"
35 #include "messaging/helpers/DialogOKHelper.h"
36 #include "settings/AdvancedSettings.h"
37 #include "settings/Settings.h"
38 #include "settings/SettingsComponent.h"
39 #include "utils/FileOperationJob.h"
40 #include "utils/FileUtils.h"
41 #include "utils/JobManager.h"
42 #include "utils/StringUtils.h"
43 #include "utils/URIUtils.h"
44 #include "utils/Variant.h"
45 #include "utils/XTimeUtils.h"
46 #include "utils/log.h"
48 #include <functional>
49 #include <mutex>
51 using namespace XFILE;
52 using namespace ADDON;
53 using namespace KODI::MESSAGING;
55 using namespace std::chrono_literals;
57 using KODI::MESSAGING::HELPERS::DialogResponse;
58 using KODI::UTILITY::TypedDigest;
60 namespace
62 class CAddonInstallJob : public CFileOperationJob
64 public:
65 CAddonInstallJob(const ADDON::AddonPtr& addon,
66 const ADDON::RepositoryPtr& repo,
67 AutoUpdateJob isAutoUpdate);
69 bool DoWork() override;
71 static constexpr const char* TYPE_DOWNLOAD = "DOWNLOAD";
72 static constexpr const char* TYPE_INSTALL = "INSTALL";
73 /*!
74 * \brief Returns the current processing type in the installation job
76 * \return The current processing type as string, can be \ref TYPE_DOWNLOAD or
77 * \ref TYPE_INSTALL
79 const char* GetType() const override { return m_currentType; }
81 /*! \brief Find the add-on and its repository for the given add-on ID
82 * \param addonID ID of the add-on to find
83 * \param[out] repo the repository to use
84 * \param[out] addon Add-on with the given add-on ID
85 * \return True if the add-on and its repository were found, false otherwise.
87 static bool GetAddon(const std::string& addonID,
88 ADDON::RepositoryPtr& repo,
89 ADDON::AddonPtr& addon);
91 void SetDependsInstall(DependencyJob dependsInstall) { m_dependsInstall = dependsInstall; }
92 void SetAllowCheckForUpdates(AllowCheckForUpdates allowCheckForUpdates)
94 m_allowCheckForUpdates = allowCheckForUpdates;
97 private:
98 void OnPreInstall();
99 void OnPostInstall();
100 bool Install(const std::string& installFrom,
101 const ADDON::RepositoryPtr& repo = ADDON::RepositoryPtr());
102 bool DownloadPackage(const std::string& path, const std::string& dest);
104 bool DoFileOperation(FileAction action,
105 CFileItemList& items,
106 const std::string& file,
107 bool useSameJob = true);
109 /*! \brief Queue a notification for addon installation/update failure
110 \param addonID - addon id
111 \param fileName - filename which is shown in case the addon id is unknown
112 \param message - error message to be displayed
114 void ReportInstallError(const std::string& addonID,
115 const std::string& fileName,
116 const std::string& message = "");
118 ADDON::AddonPtr m_addon;
119 ADDON::RepositoryPtr m_repo;
120 bool m_isUpdate;
121 AutoUpdateJob m_isAutoUpdate;
122 DependencyJob m_dependsInstall = DependencyJob::CHOICE_NO;
123 AllowCheckForUpdates m_allowCheckForUpdates = AllowCheckForUpdates::CHOICE_YES;
124 const char* m_currentType = TYPE_DOWNLOAD;
127 class CAddonUnInstallJob : public CFileOperationJob
129 public:
130 CAddonUnInstallJob(const ADDON::AddonPtr& addon, bool removeData);
132 bool DoWork() override;
133 void SetRecurseOrphaned(RecurseOrphaned recurseOrphaned) { m_recurseOrphaned = recurseOrphaned; };
135 private:
136 void ClearFavourites();
138 ADDON::AddonPtr m_addon;
139 bool m_removeData;
140 RecurseOrphaned m_recurseOrphaned = RecurseOrphaned::CHOICE_YES;
143 } // unnamed namespace
145 CAddonInstaller::CAddonInstaller() : m_idle(true)
148 CAddonInstaller::~CAddonInstaller() = default;
150 CAddonInstaller &CAddonInstaller::GetInstance()
152 static CAddonInstaller addonInstaller;
153 return addonInstaller;
156 void CAddonInstaller::OnJobComplete(unsigned int jobID, bool success, CJob* job)
158 std::unique_lock<CCriticalSection> lock(m_critSection);
159 JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair<std::string, CDownloadJob>& p) {
160 return p.second.jobID == jobID;
162 if (i != m_downloadJobs.end())
163 m_downloadJobs.erase(i);
164 if (m_downloadJobs.empty())
165 m_idle.Set();
166 lock.unlock();
167 PrunePackageCache();
169 CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
170 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
173 void CAddonInstaller::OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job)
175 std::unique_lock<CCriticalSection> lock(m_critSection);
176 JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair<std::string, CDownloadJob>& p) {
177 return p.second.jobID == jobID;
179 if (i != m_downloadJobs.end())
181 // update job progress
182 i->second.progress = 100 / total * progress;
183 i->second.downloadFinshed = std::string(job->GetType()) == CAddonInstallJob::TYPE_INSTALL;
184 CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM);
185 msg.SetStringParam(i->first);
186 lock.unlock();
187 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
191 bool CAddonInstaller::IsDownloading() const
193 std::unique_lock<CCriticalSection> lock(m_critSection);
194 return !m_downloadJobs.empty();
197 void CAddonInstaller::GetInstallList(VECADDONS &addons) const
199 std::unique_lock<CCriticalSection> lock(m_critSection);
200 std::vector<std::string> addonIDs;
201 for (JobMap::const_iterator i = m_downloadJobs.begin(); i != m_downloadJobs.end(); ++i)
203 if (i->second.jobID)
204 addonIDs.push_back(i->first);
206 lock.unlock();
208 auto& addonMgr = CServiceBroker::GetAddonMgr();
209 for (const auto& addonId : addonIDs)
211 AddonPtr addon;
212 if (addonMgr.FindInstallableById(addonId, addon))
213 addons.emplace_back(std::move(addon));
217 bool CAddonInstaller::GetProgress(const std::string& addonID, unsigned int& percent, bool& downloadFinshed) const
219 std::unique_lock<CCriticalSection> lock(m_critSection);
220 JobMap::const_iterator i = m_downloadJobs.find(addonID);
221 if (i != m_downloadJobs.end())
223 percent = i->second.progress;
224 downloadFinshed = i->second.downloadFinshed;
225 return true;
227 return false;
230 bool CAddonInstaller::Cancel(const std::string &addonID)
232 std::unique_lock<CCriticalSection> lock(m_critSection);
233 JobMap::iterator i = m_downloadJobs.find(addonID);
234 if (i != m_downloadJobs.end())
236 CServiceBroker::GetJobManager()->CancelJob(i->second.jobID);
237 m_downloadJobs.erase(i);
238 if (m_downloadJobs.empty())
239 m_idle.Set();
240 return true;
243 return false;
246 bool CAddonInstaller::InstallModal(const std::string& addonID,
247 ADDON::AddonPtr& addon,
248 InstallModalPrompt promptForInstall)
250 if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
251 return false;
253 // we assume that addons that are enabled don't get to this routine (i.e. that GetAddon() has been called)
254 if (CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, OnlyEnabled::CHOICE_NO))
255 return false; // addon is installed but disabled, and the user has specifically activated something that needs
256 // the addon - should we enable it?
258 // check we have it available
259 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon))
260 return false;
262 // if specified ask the user if he wants it installed
263 if (promptForInstall == InstallModalPrompt::CHOICE_YES)
265 if (HELPERS::ShowYesNoDialogLines(CVariant{24076}, CVariant{24100}, CVariant{addon->Name()},
266 CVariant{24101}) != DialogResponse::CHOICE_YES)
268 return false;
272 if (!InstallOrUpdate(addonID, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_YES))
273 return false;
275 return CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, OnlyEnabled::CHOICE_YES);
279 bool CAddonInstaller::InstallOrUpdate(const std::string& addonID,
280 BackgroundJob background,
281 ModalJob modal)
283 AddonPtr addon;
284 RepositoryPtr repo;
285 if (!CAddonInstallJob::GetAddon(addonID, repo, addon))
286 return false;
288 return DoInstall(addon, repo, background, modal, AutoUpdateJob::CHOICE_NO,
289 DependencyJob::CHOICE_NO, AllowCheckForUpdates::CHOICE_YES);
292 bool CAddonInstaller::InstallOrUpdateDependency(const ADDON::AddonPtr& dependsId,
293 const ADDON::RepositoryPtr& repo)
295 return DoInstall(dependsId, repo, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_NO,
296 AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_YES,
297 AllowCheckForUpdates::CHOICE_YES);
300 bool CAddonInstaller::RemoveDependency(const std::shared_ptr<IAddon>& dependsId) const
302 const bool removeData = CDirectory::Exists(dependsId->Profile());
303 CAddonUnInstallJob removeDependencyJob(dependsId, removeData);
304 removeDependencyJob.SetRecurseOrphaned(RecurseOrphaned::CHOICE_NO);
306 return removeDependencyJob.DoWork();
309 std::vector<std::string> CAddonInstaller::RemoveOrphanedDepsRecursively() const
311 std::vector<std::string> removedItems;
313 auto toRemove = CServiceBroker::GetAddonMgr().GetOrphanedDependencies();
314 while (toRemove.size() > 0)
316 for (const auto& dep : toRemove)
318 if (RemoveDependency(dep))
320 removedItems.emplace_back(dep->Name()); // successfully removed
322 else
324 CLog::Log(LOGERROR, "CAddonMgr::{}: failed to remove orphaned add-on/dependency: {}",
325 __func__, dep->Name());
329 toRemove = CServiceBroker::GetAddonMgr().GetOrphanedDependencies();
332 return removedItems;
335 bool CAddonInstaller::Install(const std::string& addonId,
336 const CAddonVersion& version,
337 const std::string& repoId)
339 CLog::Log(LOGDEBUG, "CAddonInstaller: installing '{}' version '{}' from repository '{}'", addonId,
340 version.asString(), repoId);
342 AddonPtr addon;
343 CAddonDatabase database;
345 if (!database.Open() || !database.GetAddon(addonId, version, repoId, addon))
346 return false;
348 AddonPtr repo;
349 if (!CServiceBroker::GetAddonMgr().GetAddon(repoId, repo, AddonType::REPOSITORY,
350 OnlyEnabled::CHOICE_YES))
351 return false;
353 return DoInstall(addon, std::static_pointer_cast<CRepository>(repo), BackgroundJob::CHOICE_YES,
354 ModalJob::CHOICE_NO, AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO,
355 AllowCheckForUpdates::CHOICE_YES);
358 bool CAddonInstaller::DoInstall(const AddonPtr& addon,
359 const RepositoryPtr& repo,
360 BackgroundJob background,
361 ModalJob modal,
362 AutoUpdateJob autoUpdate,
363 DependencyJob dependsInstall,
364 AllowCheckForUpdates allowCheckForUpdates)
366 // check whether we already have the addon installing
367 std::unique_lock<CCriticalSection> lock(m_critSection);
368 if (m_downloadJobs.find(addon->ID()) != m_downloadJobs.end())
369 return false;
371 CAddonInstallJob* installJob = new CAddonInstallJob(addon, repo, autoUpdate);
372 if (background == BackgroundJob::CHOICE_YES)
374 // Workaround: because CAddonInstallJob is blocking waiting for other jobs, it needs to be run
375 // with priority dedicated.
376 unsigned int jobID =
377 CServiceBroker::GetJobManager()->AddJob(installJob, this, CJob::PRIORITY_DEDICATED);
378 m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(jobID)));
379 m_idle.Reset();
381 return true;
384 m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(0)));
385 m_idle.Reset();
386 lock.unlock();
388 installJob->SetDependsInstall(dependsInstall);
389 installJob->SetAllowCheckForUpdates(allowCheckForUpdates);
391 bool result = false;
392 if (modal == ModalJob::CHOICE_YES)
393 result = installJob->DoModal();
394 else
395 result = installJob->DoWork();
396 delete installJob;
398 lock.lock();
399 JobMap::iterator i = m_downloadJobs.find(addon->ID());
400 m_downloadJobs.erase(i);
401 if (m_downloadJobs.empty())
402 m_idle.Set();
404 return result;
407 bool CAddonInstaller::InstallFromZip(const std::string &path)
409 if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
410 return false;
412 CLog::Log(LOGDEBUG, "CAddonInstaller: installing from zip '{}'", CURL::GetRedacted(path));
414 // grab the descriptive XML document from the zip, and read it in
415 CFileItemList items;
416 //! @bug some zip files return a single item (root folder) that we think is stored, so we don't use the zip:// protocol
417 CURL pathToUrl(path);
418 CURL zipDir = URIUtils::CreateArchivePath("zip", pathToUrl, "");
419 auto eventLog = CServiceBroker::GetEventLog();
420 if (!CDirectory::GetDirectory(zipDir, items, "", DIR_FLAG_DEFAULTS) ||
421 items.Size() != 1 || !items[0]->m_bIsFolder)
423 if (eventLog)
424 eventLog->AddWithNotification(EventPtr(
425 new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings.Get(24143), path),
426 "special://xbmc/media/icon256x256.png", EventLevel::Error)));
428 CLog::Log(
429 LOGERROR,
430 "CAddonInstaller: installing addon failed '{}' - itemsize: {}, first item is folder: {}",
431 CURL::GetRedacted(path), items.Size(), items[0]->m_bIsFolder);
432 return false;
435 AddonPtr addon;
436 if (CServiceBroker::GetAddonMgr().LoadAddonDescription(items[0]->GetPath(), addon))
437 return DoInstall(addon, RepositoryPtr(), BackgroundJob::CHOICE_YES, ModalJob::CHOICE_NO,
438 AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO,
439 AllowCheckForUpdates::CHOICE_YES);
441 if (eventLog)
442 eventLog->AddWithNotification(EventPtr(
443 new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings.Get(24143), path),
444 "special://xbmc/media/icon256x256.png", EventLevel::Error)));
445 return false;
448 bool CAddonInstaller::UnInstall(const AddonPtr& addon, bool removeData)
450 CServiceBroker::GetJobManager()->AddJob(new CAddonUnInstallJob(addon, removeData), this);
451 return true;
454 bool CAddonInstaller::CheckDependencies(const AddonPtr& addon,
455 CAddonDatabase* database /* = nullptr */)
457 std::pair<std::string, std::string> failedDep;
458 return CheckDependencies(addon, failedDep, database);
461 bool CAddonInstaller::CheckDependencies(const AddonPtr& addon,
462 std::pair<std::string, std::string>& failedDep,
463 CAddonDatabase* database /* = nullptr */)
465 std::vector<std::string> preDeps;
466 preDeps.push_back(addon->ID());
467 CAddonDatabase localDB;
468 if (!database)
469 database = &localDB;
471 return CheckDependencies(addon, preDeps, *database, failedDep);
474 bool CAddonInstaller::CheckDependencies(const AddonPtr &addon,
475 std::vector<std::string>& preDeps, CAddonDatabase &database,
476 std::pair<std::string, std::string> &failedDep)
478 if (addon == nullptr)
479 return true; // a nullptr addon has no dependencies
481 for (const auto& it : addon->GetDependencies())
483 const std::string &addonID = it.id;
484 const CAddonVersion& versionMin = it.versionMin;
485 const CAddonVersion& version = it.version;
486 bool optional = it.optional;
487 AddonPtr dep;
488 const bool haveInstalledAddon =
489 CServiceBroker::GetAddonMgr().GetAddon(addonID, dep, OnlyEnabled::CHOICE_NO);
490 if ((haveInstalledAddon && !dep->MeetsVersion(versionMin, version)) ||
491 (!haveInstalledAddon && !optional))
493 // we have it but our version isn't good enough, or we don't have it and we need it
494 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, dep) ||
495 (dep && !dep->MeetsVersion(versionMin, version)))
497 // we don't have it in a repo, or we have it but the version isn't good enough, so dep isn't satisfied.
498 CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: requires {} version {} which is not available",
499 addon->ID(), addonID, version.asString());
501 // fill in the details of the failed dependency
502 failedDep.first = addonID;
503 failedDep.second = version.asString();
505 return false;
509 // need to enable the dependency
510 if (dep && CServiceBroker::GetAddonMgr().IsAddonDisabled(addonID) &&
511 !CServiceBroker::GetAddonMgr().EnableAddon(addonID))
513 return false;
516 // 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
517 //! @todo should we assume that installed deps are OK?
518 if (dep && std::find(preDeps.begin(), preDeps.end(), dep->ID()) == preDeps.end())
520 preDeps.push_back(dep->ID());
521 if (!CheckDependencies(dep, preDeps, database, failedDep))
523 database.Close();
524 return false;
528 database.Close();
530 return true;
533 bool CAddonInstaller::HasJob(const std::string& ID) const
535 std::unique_lock<CCriticalSection> lock(m_critSection);
536 return m_downloadJobs.find(ID) != m_downloadJobs.end();
539 void CAddonInstaller::PrunePackageCache()
541 std::map<std::string, std::unique_ptr<CFileItemList>> packs;
542 int64_t size = EnumeratePackageFolder(packs);
543 int64_t limit = static_cast<int64_t>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_addonPackageFolderSize) * 1024 * 1024;
544 if (size < limit)
545 return;
547 // Prune packages
548 // 1. Remove the largest packages, leaving at least 2 for each add-on
549 CFileItemList items;
550 CAddonDatabase db;
551 db.Open();
552 for (auto it = packs.begin(); it != packs.end(); ++it)
554 it->second->Sort(SortByLabel, SortOrderDescending);
555 for (int j = 2; j < it->second->Size(); j++)
556 items.Add(CFileItemPtr(new CFileItem(*it->second->Get(j))));
559 items.Sort(SortBySize, SortOrderDescending);
560 int i = 0;
561 while (size > limit && i < items.Size())
563 size -= items[i]->m_dwSize;
564 db.RemovePackage(items[i]->GetPath());
565 CFileUtils::DeleteItem(items[i++]);
568 if (size > limit)
570 // 2. Remove the oldest packages (leaving least 1 for each add-on)
571 items.Clear();
572 for (auto it = packs.begin(); it != packs.end(); ++it)
574 if (it->second->Size() > 1)
575 items.Add(CFileItemPtr(new CFileItem(*it->second->Get(1))));
578 items.Sort(SortByDate, SortOrderAscending);
579 i = 0;
580 while (size > limit && i < items.Size())
582 size -= items[i]->m_dwSize;
583 db.RemovePackage(items[i]->GetPath());
584 CFileUtils::DeleteItem(items[i++]);
589 void CAddonInstaller::InstallAddons(const VECADDONS& addons,
590 bool wait,
591 AllowCheckForUpdates allowCheckForUpdates)
593 for (const auto& addon : addons)
595 AddonPtr toInstall;
596 RepositoryPtr repo;
597 if (CAddonInstallJob::GetAddon(addon->ID(), repo, toInstall))
598 DoInstall(toInstall, repo, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_NO,
599 AutoUpdateJob::CHOICE_YES, DependencyJob::CHOICE_NO, allowCheckForUpdates);
601 if (wait)
603 std::unique_lock<CCriticalSection> lock(m_critSection);
604 if (!m_downloadJobs.empty())
606 m_idle.Reset();
607 lock.unlock();
608 m_idle.Wait();
613 int64_t CAddonInstaller::EnumeratePackageFolder(
614 std::map<std::string, std::unique_ptr<CFileItemList>>& result)
616 CFileItemList items;
617 CDirectory::GetDirectory("special://home/addons/packages/",items,".zip",DIR_FLAG_NO_FILE_DIRS);
618 int64_t size = 0;
619 for (int i = 0; i < items.Size(); i++)
621 if (items[i]->m_bIsFolder)
622 continue;
624 size += items[i]->m_dwSize;
625 std::string pack,dummy;
626 CAddonVersion::SplitFileName(pack, dummy, items[i]->GetLabel());
627 if (result.find(pack) == result.end())
628 result[pack] = std::make_unique<CFileItemList>();
629 result[pack]->Add(CFileItemPtr(new CFileItem(*items[i])));
632 return size;
635 CAddonInstallJob::CAddonInstallJob(const AddonPtr& addon,
636 const RepositoryPtr& repo,
637 AutoUpdateJob isAutoUpdate)
638 : m_addon(addon), m_repo(repo), m_isAutoUpdate(isAutoUpdate)
640 AddonPtr dummy;
641 m_isUpdate = CServiceBroker::GetAddonMgr().GetAddon(addon->ID(), dummy, OnlyEnabled::CHOICE_NO);
644 bool CAddonInstallJob::GetAddon(const std::string& addonID, RepositoryPtr& repo,
645 ADDON::AddonPtr& addon)
647 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon))
648 return false;
650 AddonPtr tmp;
651 if (!CServiceBroker::GetAddonMgr().GetAddon(addon->Origin(), tmp, AddonType::REPOSITORY,
652 OnlyEnabled::CHOICE_YES))
653 return false;
655 repo = std::static_pointer_cast<CRepository>(tmp);
657 return true;
660 bool CAddonInstallJob::DoWork()
662 m_currentType = CAddonInstallJob::TYPE_DOWNLOAD;
664 SetTitle(StringUtils::Format(g_localizeStrings.Get(24057), m_addon->Name()));
665 SetProgress(0);
667 // check whether all the dependencies are available or not
668 SetText(g_localizeStrings.Get(24058));
669 std::pair<std::string, std::string> failedDep;
670 if (!CAddonInstaller::GetInstance().CheckDependencies(m_addon, failedDep))
672 std::string details =
673 StringUtils::Format(g_localizeStrings.Get(24142), failedDep.first, failedDep.second);
674 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: {}", m_addon->ID(), details);
675 ReportInstallError(m_addon->ID(), m_addon->ID(), details);
676 return false;
679 std::string installFrom;
681 // Addons are installed by downloading the .zip package on the server to the local
682 // packages folder, then extracting from the local .zip package into the addons folder
683 // Both these functions are achieved by "copying" using the vfs.
685 if (!m_repo && URIUtils::HasSlashAtEnd(m_addon->Path()))
686 { // passed in a folder - all we need do is copy it across
687 installFrom = m_addon->Path();
689 else
691 std::string path{m_addon->Path()};
692 TypedDigest hash;
693 if (m_repo)
695 CRepository::ResolveResult resolvedAddon = m_repo->ResolvePathAndHash(m_addon);
696 path = resolvedAddon.location;
697 hash = resolvedAddon.digest;
698 if (path.empty())
700 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to resolve addon install source path",
701 m_addon->ID());
702 ReportInstallError(m_addon->ID(), m_addon->ID());
703 return false;
707 CAddonDatabase db;
708 if (!db.Open())
710 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to open database", m_addon->ID());
711 ReportInstallError(m_addon->ID(), m_addon->ID());
712 return false;
715 std::string packageOriginalPath, packageFileName;
716 URIUtils::Split(path, packageOriginalPath, packageFileName);
717 // Use ChangeBasePath so the URL is decoded if necessary
718 const std::string packagePath = "special://home/addons/packages/";
719 //!@todo fix design flaw in file copying: We use CFileOperationJob to download the package from the internet
720 // to the local cache. It tries to be "smart" and decode the URL. But it never tells us what the result is,
721 // so if we try for example to download "http://localhost/a+b.zip" the result ends up in "a b.zip".
722 // First bug is that it actually decodes "+", which is not necessary except in query parts. Second bug
723 // is that we cannot know that it does this and what the result is so the package will not be found without
724 // using ChangeBasePath here (which is the same function the copying code uses and performs the translation).
725 std::string package = URIUtils::ChangeBasePath(packageOriginalPath, packageFileName, packagePath);
727 // check that we don't already have a valid copy
728 if (!hash.Empty())
730 std::string hashExisting;
731 if (db.GetPackageHash(m_addon->ID(), package, hashExisting) && hash.value != hashExisting)
733 db.RemovePackage(package);
735 if (CFile::Exists(package))
737 CFile::Delete(package);
741 // zip passed in - download + extract
742 if (!CFile::Exists(package))
744 if (!DownloadPackage(path, packagePath))
746 CFile::Delete(package);
748 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to download {}", m_addon->ID(),
749 package);
750 ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
751 return false;
755 // at this point we have the package - check that it is valid
756 SetText(g_localizeStrings.Get(24077));
757 if (!hash.Empty())
759 TypedDigest actualHash{hash.type, CUtil::GetFileDigest(package, hash.type)};
760 if (hash != actualHash)
762 CFile::Delete(package);
764 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: Hash mismatch after download. Expected {}, was {}",
765 m_addon->ID(), hash.value, actualHash.value);
766 ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
767 return false;
770 db.AddPackage(m_addon->ID(), package, hash.value);
773 // check if the archive is valid
774 CURL archive = URIUtils::CreateArchivePath("zip", CURL(package), "");
776 CFileItemList archivedFiles;
777 AddonPtr temp;
778 if (!CDirectory::GetDirectory(archive, archivedFiles, "", DIR_FLAG_DEFAULTS) ||
779 archivedFiles.Size() != 1 || !archivedFiles[0]->m_bIsFolder ||
780 !CServiceBroker::GetAddonMgr().LoadAddonDescription(archivedFiles[0]->GetPath(), temp))
782 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: invalid package {}", m_addon->ID(), package);
783 db.RemovePackage(package);
784 CFile::Delete(package);
785 ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
786 return false;
789 installFrom = package;
793 m_currentType = CAddonInstallJob::TYPE_INSTALL;
795 // run any pre-install functions
796 ADDON::OnPreInstall(m_addon);
798 if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID()))
800 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to unload addon.", m_addon->ID());
801 return false;
804 // perform install
805 if (!Install(installFrom, m_repo))
806 return false;
808 // Load new installed and if successed replace defined m_addon here with new one
809 if (!CServiceBroker::GetAddonMgr().LoadAddon(m_addon->ID(), m_addon->Origin(),
810 m_addon->Version()) ||
811 !CServiceBroker::GetAddonMgr().GetAddon(m_addon->ID(), m_addon, OnlyEnabled::CHOICE_YES))
813 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to reload addon", m_addon->ID());
814 return false;
817 g_localizeStrings.LoadAddonStrings(URIUtils::AddFileToFolder(m_addon->Path(), "resources/language/"),
818 CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE), m_addon->ID());
820 ADDON::OnPostInstall(m_addon, m_isUpdate, IsModal());
822 // Write origin to database via addon manager, where this information is up-to-date.
823 // Needed to set origin correctly for new installed addons.
825 std::string origin;
826 if (m_addon->Origin() == ORIGIN_SYSTEM)
828 origin = ORIGIN_SYSTEM; // keep system add-on origin as ORIGIN_SYSTEM
830 else if (m_addon->HasMainType(AddonType::REPOSITORY))
832 origin = m_addon->ID(); // use own id as origin if repository
834 // if a repository is updated during the add-on migration process, we need to skip
835 // calling CheckForUpdates() on the repo to prevent deadlock issues during migration
837 if (m_allowCheckForUpdates == AllowCheckForUpdates::CHOICE_YES)
839 if (m_isUpdate)
841 CLog::Log(LOGDEBUG, "ADDONS: repository [{}] updated. now checking for content updates.",
842 m_addon->ID());
843 CServiceBroker::GetRepositoryUpdater().CheckForUpdates(
844 std::static_pointer_cast<CRepository>(m_addon), false);
847 else
849 CLog::Log(LOGDEBUG, "ADDONS: skipping CheckForUpdates() on repository [{}].", m_addon->ID());
852 else if (m_repo)
854 origin = m_repo->ID(); // use repo id as origin
857 CServiceBroker::GetAddonMgr().SetAddonOrigin(m_addon->ID(), origin, m_isUpdate);
859 if (m_dependsInstall == DependencyJob::CHOICE_YES)
861 CLog::Log(LOGDEBUG, "ADDONS: dependency [{}] will not be version checked and unpinned",
862 m_addon->ID());
864 else
866 // we only do pinning/unpinning for non-system add-ons
867 if (m_addon->Origin() != ORIGIN_SYSTEM)
869 // get all compatible versions of an addon-id regardless of their origin
870 // from all installed repositories
871 std::vector<std::shared_ptr<IAddon>> compatibleVersions =
872 CServiceBroker::GetAddonMgr().GetCompatibleVersions(m_addon->ID());
874 if (!m_addon->Origin().empty())
876 // handle add-ons that originate from a repository
878 // find the latest version for the origin we installed from
879 CAddonVersion latestVersion;
880 for (const auto& compatibleVersion : compatibleVersions)
882 if (compatibleVersion->Origin() == m_addon->Origin() &&
883 compatibleVersion->Version() > latestVersion)
885 latestVersion = compatibleVersion->Version();
889 if (m_addon->Version() == latestVersion)
891 // unpin the installed addon if it's the latest of its origin
892 CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(),
893 AddonUpdateRule::PIN_OLD_VERSION);
894 CLog::Log(LOGDEBUG, "ADDONS: unpinned Addon: [{}] Origin: [{}] Version: [{}]",
895 m_addon->ID(), m_addon->Origin(), m_addon->Version().asString());
897 else
899 // pin if it is not the latest
900 CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(),
901 AddonUpdateRule::PIN_OLD_VERSION);
902 CLog::Log(LOGDEBUG, "ADDONS: pinned Addon: [{}] Origin: [{}] Version: [{}]",
903 m_addon->ID(), m_addon->Origin(), m_addon->Version().asString());
906 else
908 // handle manually installed add-ons
910 // find the latest version of any origin/repository
911 CAddonVersion latestVersion;
912 for (const auto& compatibleVersion : compatibleVersions)
914 if (compatibleVersion->Version() > latestVersion)
916 latestVersion = compatibleVersion->Version();
920 if (m_addon->Version() < latestVersion)
922 // pin zip version if it's lesser than latest from repo(s)
923 CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(),
924 AddonUpdateRule::PIN_ZIP_INSTALL);
925 CLog::Log(LOGDEBUG, "ADDONS: pinned zip installed Addon: [{}] Version: [{}]",
926 m_addon->ID(), m_addon->Version().asString());
928 else
930 // unpin zip version if it's >= the latest from repos
931 CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(),
932 AddonUpdateRule::PIN_ZIP_INSTALL);
933 CLog::Log(LOGDEBUG, "ADDONS: unpinned zip installed Addon: [{}] Version: [{}]",
934 m_addon->ID(), m_addon->Version().asString());
940 bool notify = (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
941 CSettings::SETTING_ADDONS_NOTIFICATIONS) ||
942 m_isAutoUpdate == AutoUpdateJob::CHOICE_NO) &&
943 !IsModal() && m_dependsInstall == DependencyJob::CHOICE_NO;
944 auto eventLog = CServiceBroker::GetEventLog();
945 if (eventLog)
946 eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, m_isUpdate ? 24065 : 24084)), notify,
947 false);
949 if (m_isAutoUpdate == AutoUpdateJob::CHOICE_YES &&
950 m_addon->LifecycleState() == AddonLifecycleState::BROKEN)
952 CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: auto-disabling due to being marked as broken",
953 m_addon->ID());
954 CServiceBroker::GetAddonMgr().DisableAddon(m_addon->ID(), AddonDisabledReason::USER);
955 if (eventLog)
956 eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, 24094)), true, false);
958 else if (m_addon->LifecycleState() == AddonLifecycleState::DEPRECATED)
960 CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: installed addon marked as deprecated",
961 m_addon->ID());
962 std::string text =
963 StringUtils::Format(g_localizeStrings.Get(24168), m_addon->LifecycleStateDescription());
964 if (eventLog)
965 eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, text)), true, false);
968 // and we're done!
969 MarkFinished();
970 return true;
973 bool CAddonInstallJob::DownloadPackage(const std::string &path, const std::string &dest)
975 if (ShouldCancel(0, 1))
976 return false;
978 SetText(g_localizeStrings.Get(24078));
980 // need to download/copy the package first
981 CFileItemList list;
982 list.Add(CFileItemPtr(new CFileItem(path, false)));
983 list[0]->Select(true);
985 return DoFileOperation(CFileOperationJob::ActionReplace, list, dest, true);
988 bool CAddonInstallJob::DoFileOperation(FileAction action, CFileItemList &items, const std::string &file, bool useSameJob /* = true */)
990 bool result = false;
991 if (useSameJob)
993 SetFileOperation(action, items, file);
995 // temporarily disable auto-closing so not to close the current progress indicator
996 bool autoClose = GetAutoClose();
997 if (autoClose)
998 SetAutoClose(false);
999 // temporarily disable updating title or text
1000 bool updateInformation = GetUpdateInformation();
1001 if (updateInformation)
1002 SetUpdateInformation(false);
1004 result = CFileOperationJob::DoWork();
1006 SetUpdateInformation(updateInformation);
1007 SetAutoClose(autoClose);
1009 else
1011 CFileOperationJob job(action, items, file);
1013 // pass our progress indicators to the temporary job and only allow it to
1014 // show progress updates (no title or text changes)
1015 job.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), GetUpdateProgress(), false);
1017 result = job.DoWork();
1020 return result;
1023 bool CAddonInstallJob::Install(const std::string &installFrom, const RepositoryPtr& repo)
1025 const auto& deps = m_addon->GetDependencies();
1027 if (!deps.empty() && m_addon->HasType(AddonType::REPOSITORY))
1029 bool notSystemAddon = std::none_of(deps.begin(), deps.end(), [](const DependencyInfo& dep) {
1030 return CServiceBroker::GetAddonMgr().IsSystemAddon(dep.id);
1033 if (notSystemAddon)
1035 CLog::Log(
1036 LOGERROR,
1037 "CAddonInstallJob::{}: failed to install repository [{}]. It has dependencies defined",
1038 __func__, m_addon->ID());
1039 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24088));
1040 return false;
1044 SetText(g_localizeStrings.Get(24079));
1045 unsigned int totalSteps = static_cast<unsigned int>(deps.size()) + 1;
1046 if (ShouldCancel(0, totalSteps))
1047 return false;
1049 CAddonRepos addonRepos;
1050 if (!addonRepos.IsValid())
1051 return false;
1053 // The first thing we do is install dependencies
1054 for (auto it = deps.begin(); it != deps.end(); ++it)
1056 if (it->id != "xbmc.metadata")
1058 const std::string &addonID = it->id;
1059 const CAddonVersion& versionMin = it->versionMin;
1060 const CAddonVersion& version = it->version;
1061 bool optional = it->optional;
1062 AddonPtr dependency;
1063 const bool haveInstalledAddon =
1064 CServiceBroker::GetAddonMgr().GetAddon(addonID, dependency, OnlyEnabled::CHOICE_NO);
1065 if ((haveInstalledAddon && !dependency->MeetsVersion(versionMin, version)) ||
1066 (!haveInstalledAddon && !optional))
1068 // we have it but our version isn't good enough, or we don't have it and we need it
1070 // dependency is already queued up for install - ::Install will fail
1071 // instead we wait until the Job has finished. note that we
1072 // recall install on purpose in case prior installation failed
1073 if (CAddonInstaller::GetInstance().HasJob(addonID))
1075 while (CAddonInstaller::GetInstance().HasJob(addonID))
1076 KODI::TIME::Sleep(50ms);
1078 if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(addonID))
1080 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
1081 m_addon->ID(), addonID);
1082 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1083 return false;
1086 // don't have the addon or the addon isn't new enough - grab it (no new job for these)
1087 else
1089 RepositoryPtr repoForDep;
1090 AddonPtr dependencyToInstall;
1092 // origin of m_addon is empty at least if an addon is installed for the first time
1093 // we need to override "parentRepoId" if the passed in repo is valid.
1095 const std::string& parentRepoId =
1096 m_addon->Origin().empty() && repo ? repo->ID() : m_addon->Origin();
1098 if (!addonRepos.FindDependency(addonID, parentRepoId, dependencyToInstall, repoForDep))
1100 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to find dependency {}", m_addon->ID(),
1101 addonID);
1102 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1103 return false;
1105 else
1107 if (!dependencyToInstall->MeetsVersion(versionMin, version))
1109 CLog::Log(LOGERROR,
1110 "CAddonInstallJob[{}]: found dependency [{}/{}] doesn't meet minimum "
1111 "version [{}]",
1112 m_addon->ID(), addonID, dependencyToInstall->Version().asString(),
1113 versionMin.asString());
1114 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1115 return false;
1119 if (IsModal())
1121 CAddonInstallJob dependencyJob(dependencyToInstall, repoForDep,
1122 AutoUpdateJob::CHOICE_NO);
1123 dependencyJob.SetDependsInstall(DependencyJob::CHOICE_YES);
1125 // pass our progress indicators to the temporary job and don't allow it to
1126 // show progress or information updates (no progress, title or text changes)
1127 dependencyJob.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), false,
1128 false);
1130 if (!dependencyJob.DoModal())
1132 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
1133 m_addon->ID(), addonID);
1134 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1135 return false;
1138 else if (!CAddonInstaller::GetInstance().InstallOrUpdateDependency(dependencyToInstall,
1139 repoForDep))
1141 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
1142 m_addon->ID(), dependencyToInstall->ID());
1143 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1144 return false;
1150 if (ShouldCancel(std::distance(deps.begin(), it), totalSteps))
1151 return false;
1154 SetText(g_localizeStrings.Get(24086));
1155 SetProgress(static_cast<unsigned int>(100.0 * (totalSteps - 1.0) / totalSteps));
1157 CFilesystemInstaller fsInstaller;
1158 if (!fsInstaller.InstallToFilesystem(installFrom, m_addon->ID()))
1160 ReportInstallError(m_addon->ID(), m_addon->ID());
1161 return false;
1164 SetProgress(100);
1166 return true;
1169 void CAddonInstallJob::ReportInstallError(const std::string& addonID, const std::string& fileName, const std::string& message /* = "" */)
1171 AddonPtr addon;
1172 CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon);
1174 MarkFinished();
1176 std::string msg = message;
1177 EventPtr activity;
1178 if (addon != nullptr)
1180 AddonPtr addon2;
1181 bool success = CServiceBroker::GetAddonMgr().GetAddon(addonID, addon2, OnlyEnabled::CHOICE_YES);
1182 if (msg.empty())
1184 msg = g_localizeStrings.Get(addon2 != nullptr && success ? 113 : 114);
1187 activity = EventPtr(new CAddonManagementEvent(addon, EventLevel::Error, msg));
1188 if (IsModal())
1189 HELPERS::ShowOKDialogText(CVariant{m_addon->Name()}, CVariant{msg});
1191 else
1193 activity = EventPtr(new CNotificationEvent(
1194 24045, !msg.empty() ? msg : StringUtils::Format(g_localizeStrings.Get(24143), fileName),
1195 EventLevel::Error));
1197 if (IsModal())
1198 HELPERS::ShowOKDialogText(CVariant{fileName}, CVariant{msg});
1201 auto eventLog = CServiceBroker::GetEventLog();
1202 if (eventLog)
1203 eventLog->Add(activity, !IsModal(), false);
1206 CAddonUnInstallJob::CAddonUnInstallJob(const AddonPtr &addon, bool removeData)
1207 : m_addon(addon), m_removeData(removeData)
1210 bool CAddonUnInstallJob::DoWork()
1212 ADDON::OnPreUnInstall(m_addon);
1214 //Unregister addon with the manager to ensure nothing tries
1215 //to interact with it while we are uninstalling.
1216 if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID()))
1218 CLog::Log(LOGERROR, "CAddonUnInstallJob[{}]: failed to unload addon.", m_addon->ID());
1219 return false;
1222 CFilesystemInstaller fsInstaller;
1223 if (!fsInstaller.UnInstallFromFilesystem(m_addon->Path()))
1225 CLog::Log(LOGERROR, "CAddonUnInstallJob[{}]: could not delete addon data.", m_addon->ID());
1226 return false;
1229 ClearFavourites();
1230 if (m_removeData)
1232 CFileUtils::DeleteItem(m_addon->Profile());
1235 AddonPtr addon;
1237 // try to get the addon object from the repository as the local one does not exist anymore
1238 // if that doesn't work fall back to the local one
1239 if (!CServiceBroker::GetAddonMgr().FindInstallableById(m_addon->ID(), addon) || addon == nullptr)
1241 addon = m_addon;
1244 auto eventLog = CServiceBroker::GetEventLog();
1245 if (eventLog)
1246 eventLog->Add(EventPtr(new CAddonManagementEvent(addon, 24144))); // Add-on uninstalled
1248 CServiceBroker::GetAddonMgr().OnPostUnInstall(m_addon->ID());
1250 CAddonDatabase database;
1251 if (database.Open())
1252 database.OnPostUnInstall(m_addon->ID());
1254 ADDON::OnPostUnInstall(m_addon);
1256 if (m_recurseOrphaned == RecurseOrphaned::CHOICE_YES)
1258 const auto removedItems = CAddonInstaller::GetInstance().RemoveOrphanedDepsRecursively();
1260 if (removedItems.size() > 0)
1262 CLog::Log(LOGINFO, "CAddonUnInstallJob[{}]: removed orphaned dependencies ({})",
1263 m_addon->ID(), StringUtils::Join(removedItems, ", "));
1267 return true;
1270 void CAddonUnInstallJob::ClearFavourites()
1272 bool bSave = false;
1273 CFileItemList items;
1274 CServiceBroker::GetFavouritesService().GetAll(items);
1275 for (int i = 0; i < items.Size(); i++)
1277 if (items[i]->GetPath().find(m_addon->ID()) != std::string::npos)
1279 items.Remove(items[i].get());
1280 bSave = true;
1284 if (bSave)
1285 CServiceBroker::GetFavouritesService().Save(items);