[videodb] remove unused seasons table from episode_view
[xbmc.git] / xbmc / addons / AddonInstaller.cpp
blob3343bf1170e852f259f0dd199ed3b2b21db5cfb2
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 <memory>
50 #include <mutex>
52 using namespace XFILE;
53 using namespace ADDON;
54 using namespace KODI::MESSAGING;
56 using namespace std::chrono_literals;
58 using KODI::MESSAGING::HELPERS::DialogResponse;
59 using KODI::UTILITY::TypedDigest;
61 namespace
63 class CAddonInstallJob : public CFileOperationJob
65 public:
66 CAddonInstallJob(const ADDON::AddonPtr& addon,
67 const ADDON::RepositoryPtr& repo,
68 AutoUpdateJob isAutoUpdate);
70 bool DoWork() override;
72 static constexpr const char* TYPE_DOWNLOAD = "DOWNLOAD";
73 static constexpr const char* TYPE_INSTALL = "INSTALL";
74 /*!
75 * \brief Returns the current processing type in the installation job
77 * \return The current processing type as string, can be \ref TYPE_DOWNLOAD or
78 * \ref TYPE_INSTALL
80 const char* GetType() const override { return m_currentType; }
82 /*! \brief Find the add-on and its repository for the given add-on ID
83 * \param addonID ID of the add-on to find
84 * \param[out] repo the repository to use
85 * \param[out] addon Add-on with the given add-on ID
86 * \return True if the add-on and its repository were found, false otherwise.
88 static bool GetAddon(const std::string& addonID,
89 ADDON::RepositoryPtr& repo,
90 ADDON::AddonPtr& addon);
92 void SetDependsInstall(DependencyJob dependsInstall) { m_dependsInstall = dependsInstall; }
93 void SetAllowCheckForUpdates(AllowCheckForUpdates allowCheckForUpdates)
95 m_allowCheckForUpdates = allowCheckForUpdates;
98 private:
99 void OnPreInstall();
100 void OnPostInstall();
101 bool Install(const std::string& installFrom,
102 const ADDON::RepositoryPtr& repo = ADDON::RepositoryPtr());
103 bool DownloadPackage(const std::string& path, const std::string& dest);
105 bool DoFileOperation(FileAction action,
106 CFileItemList& items,
107 const std::string& file,
108 bool useSameJob = true);
110 /*! \brief Queue a notification for addon installation/update failure
111 \param addonID - addon id
112 \param fileName - filename which is shown in case the addon id is unknown
113 \param message - error message to be displayed
115 void ReportInstallError(const std::string& addonID,
116 const std::string& fileName,
117 const std::string& message = "");
119 ADDON::AddonPtr m_addon;
120 ADDON::RepositoryPtr m_repo;
121 bool m_isUpdate;
122 AutoUpdateJob m_isAutoUpdate;
123 DependencyJob m_dependsInstall = DependencyJob::CHOICE_NO;
124 AllowCheckForUpdates m_allowCheckForUpdates = AllowCheckForUpdates::CHOICE_YES;
125 const char* m_currentType = TYPE_DOWNLOAD;
128 class CAddonUnInstallJob : public CFileOperationJob
130 public:
131 CAddonUnInstallJob(const ADDON::AddonPtr& addon, bool removeData);
133 bool DoWork() override;
134 void SetRecurseOrphaned(RecurseOrphaned recurseOrphaned) { m_recurseOrphaned = recurseOrphaned; };
136 private:
137 void ClearFavourites();
139 ADDON::AddonPtr m_addon;
140 bool m_removeData;
141 RecurseOrphaned m_recurseOrphaned = RecurseOrphaned::CHOICE_YES;
144 } // unnamed namespace
146 CAddonInstaller::CAddonInstaller() : m_idle(true)
149 CAddonInstaller::~CAddonInstaller() = default;
151 CAddonInstaller &CAddonInstaller::GetInstance()
153 static CAddonInstaller addonInstaller;
154 return addonInstaller;
157 void CAddonInstaller::OnJobComplete(unsigned int jobID, bool success, CJob* job)
159 std::unique_lock<CCriticalSection> lock(m_critSection);
160 JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair<std::string, CDownloadJob>& p) {
161 return p.second.jobID == jobID;
163 if (i != m_downloadJobs.end())
164 m_downloadJobs.erase(i);
165 if (m_downloadJobs.empty())
166 m_idle.Set();
167 lock.unlock();
168 PrunePackageCache();
170 CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
171 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
174 void CAddonInstaller::OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job)
176 std::unique_lock<CCriticalSection> lock(m_critSection);
177 JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair<std::string, CDownloadJob>& p) {
178 return p.second.jobID == jobID;
180 if (i != m_downloadJobs.end())
182 // update job progress
183 i->second.progress = 100 / total * progress;
184 i->second.downloadFinshed = std::string(job->GetType()) == CAddonInstallJob::TYPE_INSTALL;
185 CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM);
186 msg.SetStringParam(i->first);
187 lock.unlock();
188 CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
192 bool CAddonInstaller::IsDownloading() const
194 std::unique_lock<CCriticalSection> lock(m_critSection);
195 return !m_downloadJobs.empty();
198 void CAddonInstaller::GetInstallList(VECADDONS &addons) const
200 std::unique_lock<CCriticalSection> lock(m_critSection);
201 std::vector<std::string> addonIDs;
202 for (JobMap::const_iterator i = m_downloadJobs.begin(); i != m_downloadJobs.end(); ++i)
204 if (i->second.jobID)
205 addonIDs.push_back(i->first);
207 lock.unlock();
209 auto& addonMgr = CServiceBroker::GetAddonMgr();
210 for (const auto& addonId : addonIDs)
212 AddonPtr addon;
213 if (addonMgr.FindInstallableById(addonId, addon))
214 addons.emplace_back(std::move(addon));
218 bool CAddonInstaller::GetProgress(const std::string& addonID, unsigned int& percent, bool& downloadFinshed) const
220 std::unique_lock<CCriticalSection> lock(m_critSection);
221 JobMap::const_iterator i = m_downloadJobs.find(addonID);
222 if (i != m_downloadJobs.end())
224 percent = i->second.progress;
225 downloadFinshed = i->second.downloadFinshed;
226 return true;
228 return false;
231 bool CAddonInstaller::Cancel(const std::string &addonID)
233 std::unique_lock<CCriticalSection> lock(m_critSection);
234 JobMap::iterator i = m_downloadJobs.find(addonID);
235 if (i != m_downloadJobs.end())
237 CServiceBroker::GetJobManager()->CancelJob(i->second.jobID);
238 m_downloadJobs.erase(i);
239 if (m_downloadJobs.empty())
240 m_idle.Set();
241 return true;
244 return false;
247 bool CAddonInstaller::InstallModal(const std::string& addonID,
248 ADDON::AddonPtr& addon,
249 InstallModalPrompt promptForInstall)
251 if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
252 return false;
254 // we assume that addons that are enabled don't get to this routine (i.e. that GetAddon() has been called)
255 if (CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, OnlyEnabled::CHOICE_NO))
256 return false; // addon is installed but disabled, and the user has specifically activated something that needs
257 // the addon - should we enable it?
259 // check we have it available
260 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon))
261 return false;
263 // if specified ask the user if he wants it installed
264 if (promptForInstall == InstallModalPrompt::CHOICE_YES)
266 if (HELPERS::ShowYesNoDialogLines(CVariant{24076}, CVariant{24100}, CVariant{addon->Name()},
267 CVariant{24101}) != DialogResponse::CHOICE_YES)
269 return false;
273 if (!InstallOrUpdate(addonID, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_YES))
274 return false;
276 return CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, OnlyEnabled::CHOICE_YES);
280 bool CAddonInstaller::InstallOrUpdate(const std::string& addonID,
281 BackgroundJob background,
282 ModalJob modal)
284 AddonPtr addon;
285 RepositoryPtr repo;
286 if (!CAddonInstallJob::GetAddon(addonID, repo, addon))
287 return false;
289 return DoInstall(addon, repo, background, modal, AutoUpdateJob::CHOICE_NO,
290 DependencyJob::CHOICE_NO, AllowCheckForUpdates::CHOICE_YES);
293 bool CAddonInstaller::InstallOrUpdateDependency(const ADDON::AddonPtr& dependsId,
294 const ADDON::RepositoryPtr& repo)
296 return DoInstall(dependsId, repo, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_NO,
297 AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_YES,
298 AllowCheckForUpdates::CHOICE_YES);
301 bool CAddonInstaller::RemoveDependency(const std::shared_ptr<IAddon>& dependsId) const
303 const bool removeData = CDirectory::Exists(dependsId->Profile());
304 CAddonUnInstallJob removeDependencyJob(dependsId, removeData);
305 removeDependencyJob.SetRecurseOrphaned(RecurseOrphaned::CHOICE_NO);
307 return removeDependencyJob.DoWork();
310 std::vector<std::string> CAddonInstaller::RemoveOrphanedDepsRecursively() const
312 std::vector<std::string> removedItems;
314 auto toRemove = CServiceBroker::GetAddonMgr().GetOrphanedDependencies();
315 while (toRemove.size() > 0)
317 for (const auto& dep : toRemove)
319 if (RemoveDependency(dep))
321 removedItems.emplace_back(dep->Name()); // successfully removed
323 else
325 CLog::Log(LOGERROR, "CAddonMgr::{}: failed to remove orphaned add-on/dependency: {}",
326 __func__, dep->Name());
330 toRemove = CServiceBroker::GetAddonMgr().GetOrphanedDependencies();
333 return removedItems;
336 bool CAddonInstaller::Install(const std::string& addonId,
337 const CAddonVersion& version,
338 const std::string& repoId)
340 CLog::Log(LOGDEBUG, "CAddonInstaller: installing '{}' version '{}' from repository '{}'", addonId,
341 version.asString(), repoId);
343 AddonPtr addon;
344 CAddonDatabase database;
346 if (!database.Open() || !database.GetAddon(addonId, version, repoId, addon))
347 return false;
349 AddonPtr repo;
350 if (!CServiceBroker::GetAddonMgr().GetAddon(repoId, repo, AddonType::REPOSITORY,
351 OnlyEnabled::CHOICE_YES))
352 return false;
354 return DoInstall(addon, std::static_pointer_cast<CRepository>(repo), BackgroundJob::CHOICE_YES,
355 ModalJob::CHOICE_NO, AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO,
356 AllowCheckForUpdates::CHOICE_YES);
359 bool CAddonInstaller::DoInstall(const AddonPtr& addon,
360 const RepositoryPtr& repo,
361 BackgroundJob background,
362 ModalJob modal,
363 AutoUpdateJob autoUpdate,
364 DependencyJob dependsInstall,
365 AllowCheckForUpdates allowCheckForUpdates)
367 // check whether we already have the addon installing
368 std::unique_lock<CCriticalSection> lock(m_critSection);
369 if (m_downloadJobs.find(addon->ID()) != m_downloadJobs.end())
370 return false;
372 CAddonInstallJob* installJob = new CAddonInstallJob(addon, repo, autoUpdate);
373 if (background == BackgroundJob::CHOICE_YES)
375 // Workaround: because CAddonInstallJob is blocking waiting for other jobs, it needs to be run
376 // with priority dedicated.
377 unsigned int jobID =
378 CServiceBroker::GetJobManager()->AddJob(installJob, this, CJob::PRIORITY_DEDICATED);
379 m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(jobID)));
380 m_idle.Reset();
382 return true;
385 m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(0)));
386 m_idle.Reset();
387 lock.unlock();
389 installJob->SetDependsInstall(dependsInstall);
390 installJob->SetAllowCheckForUpdates(allowCheckForUpdates);
392 bool result = false;
393 if (modal == ModalJob::CHOICE_YES)
394 result = installJob->DoModal();
395 else
396 result = installJob->DoWork();
397 delete installJob;
399 lock.lock();
400 JobMap::iterator i = m_downloadJobs.find(addon->ID());
401 m_downloadJobs.erase(i);
402 if (m_downloadJobs.empty())
403 m_idle.Set();
405 return result;
408 bool CAddonInstaller::InstallFromZip(const std::string &path)
410 if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
411 return false;
413 CLog::Log(LOGDEBUG, "CAddonInstaller: installing from zip '{}'", CURL::GetRedacted(path));
415 // grab the descriptive XML document from the zip, and read it in
416 CFileItemList items;
417 //! @bug some zip files return a single item (root folder) that we think is stored, so we don't use the zip:// protocol
418 CURL pathToUrl(path);
419 CURL zipDir = URIUtils::CreateArchivePath("zip", pathToUrl, "");
420 auto eventLog = CServiceBroker::GetEventLog();
421 if (!CDirectory::GetDirectory(zipDir, items, "", DIR_FLAG_DEFAULTS) ||
422 items.Size() != 1 || !items[0]->m_bIsFolder)
424 if (eventLog)
425 eventLog->AddWithNotification(EventPtr(
426 new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings.Get(24143), path),
427 "special://xbmc/media/icon256x256.png", EventLevel::Error)));
429 CLog::Log(
430 LOGERROR,
431 "CAddonInstaller: installing addon failed '{}' - itemsize: {}, first item is folder: {}",
432 CURL::GetRedacted(path), items.Size(), items[0]->m_bIsFolder);
433 return false;
436 AddonPtr addon;
437 if (CServiceBroker::GetAddonMgr().LoadAddonDescription(items[0]->GetPath(), addon))
438 return DoInstall(addon, RepositoryPtr(), BackgroundJob::CHOICE_YES, ModalJob::CHOICE_NO,
439 AutoUpdateJob::CHOICE_NO, DependencyJob::CHOICE_NO,
440 AllowCheckForUpdates::CHOICE_YES);
442 if (eventLog)
443 eventLog->AddWithNotification(EventPtr(
444 new CNotificationEvent(24045, StringUtils::Format(g_localizeStrings.Get(24143), path),
445 "special://xbmc/media/icon256x256.png", EventLevel::Error)));
446 return false;
449 bool CAddonInstaller::UnInstall(const AddonPtr& addon, bool removeData)
451 CServiceBroker::GetJobManager()->AddJob(new CAddonUnInstallJob(addon, removeData), this);
452 return true;
455 bool CAddonInstaller::CheckDependencies(const AddonPtr& addon,
456 CAddonDatabase* database /* = nullptr */)
458 std::pair<std::string, std::string> failedDep;
459 return CheckDependencies(addon, failedDep, database);
462 bool CAddonInstaller::CheckDependencies(const AddonPtr& addon,
463 std::pair<std::string, std::string>& failedDep,
464 CAddonDatabase* database /* = nullptr */)
466 std::vector<std::string> preDeps;
467 preDeps.push_back(addon->ID());
468 CAddonDatabase localDB;
469 if (!database)
470 database = &localDB;
472 return CheckDependencies(addon, preDeps, *database, failedDep);
475 bool CAddonInstaller::CheckDependencies(const AddonPtr &addon,
476 std::vector<std::string>& preDeps, CAddonDatabase &database,
477 std::pair<std::string, std::string> &failedDep)
479 if (addon == nullptr)
480 return true; // a nullptr addon has no dependencies
482 for (const auto& it : addon->GetDependencies())
484 const std::string &addonID = it.id;
485 const CAddonVersion& versionMin = it.versionMin;
486 const CAddonVersion& version = it.version;
487 bool optional = it.optional;
488 AddonPtr dep;
489 const bool haveInstalledAddon =
490 CServiceBroker::GetAddonMgr().GetAddon(addonID, dep, OnlyEnabled::CHOICE_NO);
491 if ((haveInstalledAddon && !dep->MeetsVersion(versionMin, version)) ||
492 (!haveInstalledAddon && !optional))
494 // we have it but our version isn't good enough, or we don't have it and we need it
495 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, dep) ||
496 (dep && !dep->MeetsVersion(versionMin, version)))
498 // we don't have it in a repo, or we have it but the version isn't good enough, so dep isn't satisfied.
499 CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: requires {} version {} which is not available",
500 addon->ID(), addonID, version.asString());
502 // fill in the details of the failed dependency
503 failedDep.first = addonID;
504 failedDep.second = version.asString();
506 return false;
510 // need to enable the dependency
511 if (dep && CServiceBroker::GetAddonMgr().IsAddonDisabled(addonID) &&
512 !CServiceBroker::GetAddonMgr().EnableAddon(addonID))
514 return false;
517 // 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
518 //! @todo should we assume that installed deps are OK?
519 if (dep && std::find(preDeps.begin(), preDeps.end(), dep->ID()) == preDeps.end())
521 preDeps.push_back(dep->ID());
522 if (!CheckDependencies(dep, preDeps, database, failedDep))
524 database.Close();
525 return false;
529 database.Close();
531 return true;
534 bool CAddonInstaller::HasJob(const std::string& ID) const
536 std::unique_lock<CCriticalSection> lock(m_critSection);
537 return m_downloadJobs.find(ID) != m_downloadJobs.end();
540 void CAddonInstaller::PrunePackageCache()
542 std::map<std::string, std::unique_ptr<CFileItemList>> packs;
543 int64_t size = EnumeratePackageFolder(packs);
544 int64_t limit = static_cast<int64_t>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_addonPackageFolderSize) * 1024 * 1024;
545 if (size < limit)
546 return;
548 // Prune packages
549 // 1. Remove the largest packages, leaving at least 2 for each add-on
550 CFileItemList items;
551 CAddonDatabase db;
552 db.Open();
553 for (auto it = packs.begin(); it != packs.end(); ++it)
555 it->second->Sort(SortByLabel, SortOrderDescending);
556 for (int j = 2; j < it->second->Size(); j++)
557 items.Add(std::make_shared<CFileItem>(*it->second->Get(j)));
560 items.Sort(SortBySize, SortOrderDescending);
561 int i = 0;
562 while (size > limit && i < items.Size())
564 size -= items[i]->m_dwSize;
565 db.RemovePackage(items[i]->GetPath());
566 CFileUtils::DeleteItem(items[i++]);
569 if (size > limit)
571 // 2. Remove the oldest packages (leaving least 1 for each add-on)
572 items.Clear();
573 for (auto it = packs.begin(); it != packs.end(); ++it)
575 if (it->second->Size() > 1)
576 items.Add(std::make_shared<CFileItem>(*it->second->Get(1)));
579 items.Sort(SortByDate, SortOrderAscending);
580 i = 0;
581 while (size > limit && i < items.Size())
583 size -= items[i]->m_dwSize;
584 db.RemovePackage(items[i]->GetPath());
585 CFileUtils::DeleteItem(items[i++]);
590 void CAddonInstaller::InstallAddons(const VECADDONS& addons,
591 bool wait,
592 AllowCheckForUpdates allowCheckForUpdates)
594 for (const auto& addon : addons)
596 AddonPtr toInstall;
597 RepositoryPtr repo;
598 if (CAddonInstallJob::GetAddon(addon->ID(), repo, toInstall))
599 DoInstall(toInstall, repo, BackgroundJob::CHOICE_NO, ModalJob::CHOICE_NO,
600 AutoUpdateJob::CHOICE_YES, DependencyJob::CHOICE_NO, allowCheckForUpdates);
602 if (wait)
604 std::unique_lock<CCriticalSection> lock(m_critSection);
605 if (!m_downloadJobs.empty())
607 m_idle.Reset();
608 lock.unlock();
609 m_idle.Wait();
614 int64_t CAddonInstaller::EnumeratePackageFolder(
615 std::map<std::string, std::unique_ptr<CFileItemList>>& result)
617 CFileItemList items;
618 CDirectory::GetDirectory("special://home/addons/packages/",items,".zip",DIR_FLAG_NO_FILE_DIRS);
619 int64_t size = 0;
620 for (int i = 0; i < items.Size(); i++)
622 if (items[i]->m_bIsFolder)
623 continue;
625 size += items[i]->m_dwSize;
626 std::string pack,dummy;
627 CAddonVersion::SplitFileName(pack, dummy, items[i]->GetLabel());
628 if (result.find(pack) == result.end())
629 result[pack] = std::make_unique<CFileItemList>();
630 result[pack]->Add(std::make_shared<CFileItem>(*items[i]));
633 return size;
636 CAddonInstallJob::CAddonInstallJob(const AddonPtr& addon,
637 const RepositoryPtr& repo,
638 AutoUpdateJob isAutoUpdate)
639 : m_addon(addon), m_repo(repo), m_isAutoUpdate(isAutoUpdate)
641 AddonPtr dummy;
642 m_isUpdate = CServiceBroker::GetAddonMgr().GetAddon(addon->ID(), dummy, OnlyEnabled::CHOICE_NO);
645 bool CAddonInstallJob::GetAddon(const std::string& addonID, RepositoryPtr& repo,
646 ADDON::AddonPtr& addon)
648 if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon))
649 return false;
651 AddonPtr tmp;
652 if (!CServiceBroker::GetAddonMgr().GetAddon(addon->Origin(), tmp, AddonType::REPOSITORY,
653 OnlyEnabled::CHOICE_YES))
654 return false;
656 repo = std::static_pointer_cast<CRepository>(tmp);
658 return true;
661 bool CAddonInstallJob::DoWork()
663 m_currentType = CAddonInstallJob::TYPE_DOWNLOAD;
665 SetTitle(StringUtils::Format(g_localizeStrings.Get(24057), m_addon->Name()));
666 SetProgress(0);
668 // check whether all the dependencies are available or not
669 SetText(g_localizeStrings.Get(24058));
670 std::pair<std::string, std::string> failedDep;
671 if (!CAddonInstaller::GetInstance().CheckDependencies(m_addon, failedDep))
673 std::string details =
674 StringUtils::Format(g_localizeStrings.Get(24142), failedDep.first, failedDep.second);
675 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: {}", m_addon->ID(), details);
676 ReportInstallError(m_addon->ID(), m_addon->ID(), details);
677 return false;
680 std::string installFrom;
682 // Addons are installed by downloading the .zip package on the server to the local
683 // packages folder, then extracting from the local .zip package into the addons folder
684 // Both these functions are achieved by "copying" using the vfs.
686 if (!m_repo && URIUtils::HasSlashAtEnd(m_addon->Path()))
687 { // passed in a folder - all we need do is copy it across
688 installFrom = m_addon->Path();
690 else
692 std::string path{m_addon->Path()};
693 TypedDigest hash;
694 if (m_repo)
696 CRepository::ResolveResult resolvedAddon = m_repo->ResolvePathAndHash(m_addon);
697 path = resolvedAddon.location;
698 hash = resolvedAddon.digest;
699 if (path.empty())
701 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to resolve addon install source path",
702 m_addon->ID());
703 ReportInstallError(m_addon->ID(), m_addon->ID());
704 return false;
708 CAddonDatabase db;
709 if (!db.Open())
711 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to open database", m_addon->ID());
712 ReportInstallError(m_addon->ID(), m_addon->ID());
713 return false;
716 std::string packageOriginalPath, packageFileName;
717 URIUtils::Split(path, packageOriginalPath, packageFileName);
718 // Use ChangeBasePath so the URL is decoded if necessary
719 const std::string packagePath = "special://home/addons/packages/";
720 //!@todo fix design flaw in file copying: We use CFileOperationJob to download the package from the internet
721 // to the local cache. It tries to be "smart" and decode the URL. But it never tells us what the result is,
722 // so if we try for example to download "http://localhost/a+b.zip" the result ends up in "a b.zip".
723 // First bug is that it actually decodes "+", which is not necessary except in query parts. Second bug
724 // is that we cannot know that it does this and what the result is so the package will not be found without
725 // using ChangeBasePath here (which is the same function the copying code uses and performs the translation).
726 std::string package = URIUtils::ChangeBasePath(packageOriginalPath, packageFileName, packagePath);
728 // check that we don't already have a valid copy
729 if (!hash.Empty())
731 std::string hashExisting;
732 if (db.GetPackageHash(m_addon->ID(), package, hashExisting) && hash.value != hashExisting)
734 db.RemovePackage(package);
736 if (CFile::Exists(package))
738 CFile::Delete(package);
742 // zip passed in - download + extract
743 if (!CFile::Exists(package))
745 if (!DownloadPackage(path, packagePath))
747 CFile::Delete(package);
749 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to download {}", m_addon->ID(),
750 package);
751 ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
752 return false;
756 // at this point we have the package - check that it is valid
757 SetText(g_localizeStrings.Get(24077));
758 if (!hash.Empty())
760 TypedDigest actualHash{hash.type, CUtil::GetFileDigest(package, hash.type)};
761 if (hash != actualHash)
763 CFile::Delete(package);
765 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: Hash mismatch after download. Expected {}, was {}",
766 m_addon->ID(), hash.value, actualHash.value);
767 ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
768 return false;
771 db.AddPackage(m_addon->ID(), package, hash.value);
774 // check if the archive is valid
775 CURL archive = URIUtils::CreateArchivePath("zip", CURL(package), "");
777 CFileItemList archivedFiles;
778 AddonPtr temp;
779 if (!CDirectory::GetDirectory(archive, archivedFiles, "", DIR_FLAG_DEFAULTS) ||
780 archivedFiles.Size() != 1 || !archivedFiles[0]->m_bIsFolder ||
781 !CServiceBroker::GetAddonMgr().LoadAddonDescription(archivedFiles[0]->GetPath(), temp))
783 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: invalid package {}", m_addon->ID(), package);
784 db.RemovePackage(package);
785 CFile::Delete(package);
786 ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
787 return false;
790 installFrom = package;
794 m_currentType = CAddonInstallJob::TYPE_INSTALL;
796 // run any pre-install functions
797 ADDON::OnPreInstall(m_addon);
799 if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID()))
801 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to unload addon.", m_addon->ID());
802 return false;
805 // perform install
806 if (!Install(installFrom, m_repo))
807 return false;
809 // Load new installed and if successed replace defined m_addon here with new one
810 if (!CServiceBroker::GetAddonMgr().LoadAddon(m_addon->ID(), m_addon->Origin(),
811 m_addon->Version()) ||
812 !CServiceBroker::GetAddonMgr().GetAddon(m_addon->ID(), m_addon, OnlyEnabled::CHOICE_YES))
814 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to reload addon", m_addon->ID());
815 return false;
818 g_localizeStrings.LoadAddonStrings(URIUtils::AddFileToFolder(m_addon->Path(), "resources/language/"),
819 CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE), m_addon->ID());
821 ADDON::OnPostInstall(m_addon, m_isUpdate, IsModal());
823 // Write origin to database via addon manager, where this information is up-to-date.
824 // Needed to set origin correctly for new installed addons.
826 std::string origin;
827 if (m_addon->Origin() == ORIGIN_SYSTEM)
829 origin = ORIGIN_SYSTEM; // keep system add-on origin as ORIGIN_SYSTEM
831 else if (m_addon->HasMainType(AddonType::REPOSITORY))
833 origin = m_addon->ID(); // use own id as origin if repository
835 // if a repository is updated during the add-on migration process, we need to skip
836 // calling CheckForUpdates() on the repo to prevent deadlock issues during migration
838 if (m_allowCheckForUpdates == AllowCheckForUpdates::CHOICE_YES)
840 if (m_isUpdate)
842 CLog::Log(LOGDEBUG, "ADDONS: repository [{}] updated. now checking for content updates.",
843 m_addon->ID());
844 CServiceBroker::GetRepositoryUpdater().CheckForUpdates(
845 std::static_pointer_cast<CRepository>(m_addon), false);
848 else
850 CLog::Log(LOGDEBUG, "ADDONS: skipping CheckForUpdates() on repository [{}].", m_addon->ID());
853 else if (m_repo)
855 origin = m_repo->ID(); // use repo id as origin
858 CServiceBroker::GetAddonMgr().SetAddonOrigin(m_addon->ID(), origin, m_isUpdate);
860 if (m_dependsInstall == DependencyJob::CHOICE_YES)
862 CLog::Log(LOGDEBUG, "ADDONS: dependency [{}] will not be version checked and unpinned",
863 m_addon->ID());
865 else
867 // we only do pinning/unpinning for non-system add-ons
868 if (m_addon->Origin() != ORIGIN_SYSTEM)
870 // get all compatible versions of an addon-id regardless of their origin
871 // from all installed repositories
872 std::vector<std::shared_ptr<IAddon>> compatibleVersions =
873 CServiceBroker::GetAddonMgr().GetCompatibleVersions(m_addon->ID());
875 if (!m_addon->Origin().empty())
877 // handle add-ons that originate from a repository
879 // find the latest version for the origin we installed from
880 CAddonVersion latestVersion;
881 for (const auto& compatibleVersion : compatibleVersions)
883 if (compatibleVersion->Origin() == m_addon->Origin() &&
884 compatibleVersion->Version() > latestVersion)
886 latestVersion = compatibleVersion->Version();
890 if (m_addon->Version() == latestVersion)
892 // unpin the installed addon if it's the latest of its origin
893 CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(),
894 AddonUpdateRule::PIN_OLD_VERSION);
895 CLog::Log(LOGDEBUG, "ADDONS: unpinned Addon: [{}] Origin: [{}] Version: [{}]",
896 m_addon->ID(), m_addon->Origin(), m_addon->Version().asString());
898 else
900 // pin if it is not the latest
901 CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(),
902 AddonUpdateRule::PIN_OLD_VERSION);
903 CLog::Log(LOGDEBUG, "ADDONS: pinned Addon: [{}] Origin: [{}] Version: [{}]",
904 m_addon->ID(), m_addon->Origin(), m_addon->Version().asString());
907 else
909 // handle manually installed add-ons
911 // find the latest version of any origin/repository
912 CAddonVersion latestVersion;
913 for (const auto& compatibleVersion : compatibleVersions)
915 if (compatibleVersion->Version() > latestVersion)
917 latestVersion = compatibleVersion->Version();
921 if (m_addon->Version() < latestVersion)
923 // pin zip version if it's lesser than latest from repo(s)
924 CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(),
925 AddonUpdateRule::PIN_ZIP_INSTALL);
926 CLog::Log(LOGDEBUG, "ADDONS: pinned zip installed Addon: [{}] Version: [{}]",
927 m_addon->ID(), m_addon->Version().asString());
929 else
931 // unpin zip version if it's >= the latest from repos
932 CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(),
933 AddonUpdateRule::PIN_ZIP_INSTALL);
934 CLog::Log(LOGDEBUG, "ADDONS: unpinned zip installed Addon: [{}] Version: [{}]",
935 m_addon->ID(), m_addon->Version().asString());
941 bool notify = (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
942 CSettings::SETTING_ADDONS_NOTIFICATIONS) ||
943 m_isAutoUpdate == AutoUpdateJob::CHOICE_NO) &&
944 !IsModal() && m_dependsInstall == DependencyJob::CHOICE_NO;
945 auto eventLog = CServiceBroker::GetEventLog();
946 if (eventLog)
947 eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, m_isUpdate ? 24065 : 24084)), notify,
948 false);
950 if (m_isAutoUpdate == AutoUpdateJob::CHOICE_YES &&
951 m_addon->LifecycleState() == AddonLifecycleState::BROKEN)
953 CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: auto-disabling due to being marked as broken",
954 m_addon->ID());
955 CServiceBroker::GetAddonMgr().DisableAddon(m_addon->ID(), AddonDisabledReason::USER);
956 if (eventLog)
957 eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, 24094)), true, false);
959 else if (m_addon->LifecycleState() == AddonLifecycleState::DEPRECATED)
961 CLog::Log(LOGDEBUG, "CAddonInstallJob[{}]: installed addon marked as deprecated",
962 m_addon->ID());
963 std::string text =
964 StringUtils::Format(g_localizeStrings.Get(24168), m_addon->LifecycleStateDescription());
965 if (eventLog)
966 eventLog->Add(EventPtr(new CAddonManagementEvent(m_addon, text)), true, false);
969 // and we're done!
970 MarkFinished();
971 return true;
974 bool CAddonInstallJob::DownloadPackage(const std::string &path, const std::string &dest)
976 if (ShouldCancel(0, 1))
977 return false;
979 SetText(g_localizeStrings.Get(24078));
981 // need to download/copy the package first
982 CFileItemList list;
983 list.Add(std::make_shared<CFileItem>(path, false));
984 list[0]->Select(true);
986 return DoFileOperation(CFileOperationJob::ActionReplace, list, dest, true);
989 bool CAddonInstallJob::DoFileOperation(FileAction action, CFileItemList &items, const std::string &file, bool useSameJob /* = true */)
991 bool result = false;
992 if (useSameJob)
994 SetFileOperation(action, items, file);
996 // temporarily disable auto-closing so not to close the current progress indicator
997 bool autoClose = GetAutoClose();
998 if (autoClose)
999 SetAutoClose(false);
1000 // temporarily disable updating title or text
1001 bool updateInformation = GetUpdateInformation();
1002 if (updateInformation)
1003 SetUpdateInformation(false);
1005 result = CFileOperationJob::DoWork();
1007 SetUpdateInformation(updateInformation);
1008 SetAutoClose(autoClose);
1010 else
1012 CFileOperationJob job(action, items, file);
1014 // pass our progress indicators to the temporary job and only allow it to
1015 // show progress updates (no title or text changes)
1016 job.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), GetUpdateProgress(), false);
1018 result = job.DoWork();
1021 return result;
1024 bool CAddonInstallJob::Install(const std::string &installFrom, const RepositoryPtr& repo)
1026 const auto& deps = m_addon->GetDependencies();
1028 if (!deps.empty() && m_addon->HasType(AddonType::REPOSITORY))
1030 bool notSystemAddon = std::none_of(deps.begin(), deps.end(), [](const DependencyInfo& dep) {
1031 return CServiceBroker::GetAddonMgr().IsSystemAddon(dep.id);
1034 if (notSystemAddon)
1036 CLog::Log(
1037 LOGERROR,
1038 "CAddonInstallJob::{}: failed to install repository [{}]. It has dependencies defined",
1039 __func__, m_addon->ID());
1040 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24088));
1041 return false;
1045 SetText(g_localizeStrings.Get(24079));
1046 unsigned int totalSteps = static_cast<unsigned int>(deps.size()) + 1;
1047 if (ShouldCancel(0, totalSteps))
1048 return false;
1050 CAddonRepos addonRepos;
1051 if (!addonRepos.IsValid())
1052 return false;
1054 // The first thing we do is install dependencies
1055 for (auto it = deps.begin(); it != deps.end(); ++it)
1057 if (it->id != "xbmc.metadata")
1059 const std::string &addonID = it->id;
1060 const CAddonVersion& versionMin = it->versionMin;
1061 const CAddonVersion& version = it->version;
1062 bool optional = it->optional;
1063 AddonPtr dependency;
1064 const bool haveInstalledAddon =
1065 CServiceBroker::GetAddonMgr().GetAddon(addonID, dependency, OnlyEnabled::CHOICE_NO);
1066 if ((haveInstalledAddon && !dependency->MeetsVersion(versionMin, version)) ||
1067 (!haveInstalledAddon && !optional))
1069 // we have it but our version isn't good enough, or we don't have it and we need it
1071 // dependency is already queued up for install - ::Install will fail
1072 // instead we wait until the Job has finished. note that we
1073 // recall install on purpose in case prior installation failed
1074 if (CAddonInstaller::GetInstance().HasJob(addonID))
1076 while (CAddonInstaller::GetInstance().HasJob(addonID))
1077 KODI::TIME::Sleep(50ms);
1079 if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(addonID))
1081 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
1082 m_addon->ID(), addonID);
1083 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1084 return false;
1087 // don't have the addon or the addon isn't new enough - grab it (no new job for these)
1088 else
1090 RepositoryPtr repoForDep;
1091 AddonPtr dependencyToInstall;
1093 // origin of m_addon is empty at least if an addon is installed for the first time
1094 // we need to override "parentRepoId" if the passed in repo is valid.
1096 const std::string& parentRepoId =
1097 m_addon->Origin().empty() && repo ? repo->ID() : m_addon->Origin();
1099 if (!addonRepos.FindDependency(addonID, parentRepoId, dependencyToInstall, repoForDep))
1101 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to find dependency {}", m_addon->ID(),
1102 addonID);
1103 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1104 return false;
1106 else
1108 if (!dependencyToInstall->MeetsVersion(versionMin, version))
1110 CLog::Log(LOGERROR,
1111 "CAddonInstallJob[{}]: found dependency [{}/{}] doesn't meet minimum "
1112 "version [{}]",
1113 m_addon->ID(), addonID, dependencyToInstall->Version().asString(),
1114 versionMin.asString());
1115 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1116 return false;
1120 if (IsModal())
1122 CAddonInstallJob dependencyJob(dependencyToInstall, repoForDep,
1123 AutoUpdateJob::CHOICE_NO);
1124 dependencyJob.SetDependsInstall(DependencyJob::CHOICE_YES);
1126 // pass our progress indicators to the temporary job and don't allow it to
1127 // show progress or information updates (no progress, title or text changes)
1128 dependencyJob.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), false,
1129 false);
1131 if (!dependencyJob.DoModal())
1133 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
1134 m_addon->ID(), addonID);
1135 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1136 return false;
1139 else if (!CAddonInstaller::GetInstance().InstallOrUpdateDependency(dependencyToInstall,
1140 repoForDep))
1142 CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
1143 m_addon->ID(), dependencyToInstall->ID());
1144 ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
1145 return false;
1151 if (ShouldCancel(std::distance(deps.begin(), it), totalSteps))
1152 return false;
1155 SetText(g_localizeStrings.Get(24086));
1156 SetProgress(static_cast<unsigned int>(100.0 * (totalSteps - 1.0) / totalSteps));
1158 CFilesystemInstaller fsInstaller;
1159 if (!fsInstaller.InstallToFilesystem(installFrom, m_addon->ID()))
1161 ReportInstallError(m_addon->ID(), m_addon->ID());
1162 return false;
1165 SetProgress(100);
1167 return true;
1170 void CAddonInstallJob::ReportInstallError(const std::string& addonID, const std::string& fileName, const std::string& message /* = "" */)
1172 AddonPtr addon;
1173 CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon);
1175 MarkFinished();
1177 std::string msg = message;
1178 EventPtr activity;
1179 if (addon != nullptr)
1181 AddonPtr addon2;
1182 bool success = CServiceBroker::GetAddonMgr().GetAddon(addonID, addon2, OnlyEnabled::CHOICE_YES);
1183 if (msg.empty())
1185 msg = g_localizeStrings.Get(addon2 != nullptr && success ? 113 : 114);
1188 activity = EventPtr(new CAddonManagementEvent(addon, EventLevel::Error, msg));
1189 if (IsModal())
1190 HELPERS::ShowOKDialogText(CVariant{m_addon->Name()}, CVariant{msg});
1192 else
1194 activity = EventPtr(new CNotificationEvent(
1195 24045, !msg.empty() ? msg : StringUtils::Format(g_localizeStrings.Get(24143), fileName),
1196 EventLevel::Error));
1198 if (IsModal())
1199 HELPERS::ShowOKDialogText(CVariant{fileName}, CVariant{msg});
1202 auto eventLog = CServiceBroker::GetEventLog();
1203 if (eventLog)
1204 eventLog->Add(activity, !IsModal(), false);
1207 CAddonUnInstallJob::CAddonUnInstallJob(const AddonPtr &addon, bool removeData)
1208 : m_addon(addon), m_removeData(removeData)
1211 bool CAddonUnInstallJob::DoWork()
1213 ADDON::OnPreUnInstall(m_addon);
1215 //Unregister addon with the manager to ensure nothing tries
1216 //to interact with it while we are uninstalling.
1217 if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID()))
1219 CLog::Log(LOGERROR, "CAddonUnInstallJob[{}]: failed to unload addon.", m_addon->ID());
1220 return false;
1223 CFilesystemInstaller fsInstaller;
1224 if (!fsInstaller.UnInstallFromFilesystem(m_addon->Path()))
1226 CLog::Log(LOGERROR, "CAddonUnInstallJob[{}]: could not delete addon data.", m_addon->ID());
1227 return false;
1230 ClearFavourites();
1231 if (m_removeData)
1233 CFileUtils::DeleteItem(m_addon->Profile());
1236 AddonPtr addon;
1238 // try to get the addon object from the repository as the local one does not exist anymore
1239 // if that doesn't work fall back to the local one
1240 if (!CServiceBroker::GetAddonMgr().FindInstallableById(m_addon->ID(), addon) || addon == nullptr)
1242 addon = m_addon;
1245 auto eventLog = CServiceBroker::GetEventLog();
1246 if (eventLog)
1247 eventLog->Add(EventPtr(new CAddonManagementEvent(addon, 24144))); // Add-on uninstalled
1249 CServiceBroker::GetAddonMgr().OnPostUnInstall(m_addon->ID());
1251 CAddonDatabase database;
1252 if (database.Open())
1253 database.OnPostUnInstall(m_addon->ID());
1255 ADDON::OnPostUnInstall(m_addon);
1257 if (m_recurseOrphaned == RecurseOrphaned::CHOICE_YES)
1259 const auto removedItems = CAddonInstaller::GetInstance().RemoveOrphanedDepsRecursively();
1261 if (removedItems.size() > 0)
1263 CLog::Log(LOGINFO, "CAddonUnInstallJob[{}]: removed orphaned dependencies ({})",
1264 m_addon->ID(), StringUtils::Join(removedItems, ", "));
1268 return true;
1271 void CAddonUnInstallJob::ClearFavourites()
1273 bool bSave = false;
1274 CFileItemList items;
1275 CServiceBroker::GetFavouritesService().GetAll(items);
1276 for (int i = 0; i < items.Size(); i++)
1278 if (items[i]->GetPath().find(m_addon->ID()) != std::string::npos)
1280 items.Remove(items[i].get());
1281 bSave = true;
1285 if (bSave)
1286 CServiceBroker::GetFavouritesService().Save(items);