1 /*
2  *  Copyright (C) 2011-2018 Team Kodi
3  *  This file is part of Kodi - https://kodi.tv
4  *
5  *  SPDX-License-Identifier: GPL-2.0-or-later
6  *  See LICENSES/README.md for more information.
7  */
8 
9 #include "AddonInstaller.h"
10 
11 #include "FileItem.h"
12 #include "FilesystemInstaller.h"
13 #include "GUIPassword.h"
14 #include "GUIUserMessages.h" // for callback
15 #include "RepositoryUpdater.h"
16 #include "ServiceBroker.h"
17 #include "URL.h"
18 #include "Util.h"
19 #include "addons/AddonManager.h"
20 #include "addons/AddonRepos.h"
21 #include "addons/Repository.h"
22 #include "dialogs/GUIDialogExtendedProgressBar.h"
23 #include "events/AddonManagementEvent.h"
24 #include "events/EventLog.h"
25 #include "events/NotificationEvent.h"
26 #include "favourites/FavouritesService.h"
27 #include "filesystem/Directory.h"
28 #include "guilib/GUIComponent.h"
29 #include "guilib/GUIWindowManager.h" // for callback
30 #include "guilib/LocalizeStrings.h"
31 #include "messaging/helpers/DialogHelper.h"
32 #include "messaging/helpers/DialogOKHelper.h"
33 #include "settings/AdvancedSettings.h"
34 #include "settings/Settings.h"
35 #include "settings/SettingsComponent.h"
36 #include "utils/FileUtils.h"
37 #include "utils/JobManager.h"
38 #include "utils/StringUtils.h"
39 #include "utils/URIUtils.h"
40 #include "utils/Variant.h"
41 #include "utils/XTimeUtils.h"
42 #include "utils/log.h"
43 
44 #include <functional>
45 
46 using namespace XFILE;
47 using namespace ADDON;
48 using namespace KODI::MESSAGING;
49 
50 using KODI::MESSAGING::HELPERS::DialogResponse;
51 using KODI::UTILITY::TypedDigest;
52 
CAddonInstaller()53 CAddonInstaller::CAddonInstaller() : m_idle(true)
54 { }
55 
56 CAddonInstaller::~CAddonInstaller() = default;
57 
GetInstance()58 CAddonInstaller &CAddonInstaller::GetInstance()
59 {
60   static CAddonInstaller addonInstaller;
61   return addonInstaller;
62 }
63 
OnJobComplete(unsigned int jobID,bool success,CJob * job)64 void CAddonInstaller::OnJobComplete(unsigned int jobID, bool success, CJob* job)
65 {
66   CSingleLock lock(m_critSection);
67   JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair<std::string, CDownloadJob>& p) {
68     return p.second.jobID == jobID;
69   });
70   if (i != m_downloadJobs.end())
71     m_downloadJobs.erase(i);
72   if (m_downloadJobs.empty())
73     m_idle.Set();
74   lock.Leave();
75   PrunePackageCache();
76 
77   CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE);
78   CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
79 }
80 
OnJobProgress(unsigned int jobID,unsigned int progress,unsigned int total,const CJob * job)81 void CAddonInstaller::OnJobProgress(unsigned int jobID, unsigned int progress, unsigned int total, const CJob *job)
82 {
83   CSingleLock lock(m_critSection);
84   JobMap::iterator i = find_if(m_downloadJobs.begin(), m_downloadJobs.end(), [jobID](const std::pair<std::string, CDownloadJob>& p) {
85     return p.second.jobID == jobID;
86   });
87   if (i != m_downloadJobs.end())
88   {
89     // update job progress
90     i->second.progress = 100 / total * progress;
91     i->second.downloadFinshed = std::string(job->GetType()) == CAddonInstallJob::TYPE_INSTALL;
92     CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM);
93     msg.SetStringParam(i->first);
94     lock.Leave();
95     CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg);
96   }
97 }
98 
IsDownloading() const99 bool CAddonInstaller::IsDownloading() const
100 {
101   CSingleLock lock(m_critSection);
102   return !m_downloadJobs.empty();
103 }
104 
GetInstallList(VECADDONS & addons) const105 void CAddonInstaller::GetInstallList(VECADDONS &addons) const
106 {
107   CSingleLock lock(m_critSection);
108   std::vector<std::string> addonIDs;
109   for (JobMap::const_iterator i = m_downloadJobs.begin(); i != m_downloadJobs.end(); ++i)
110   {
111     if (i->second.jobID)
112       addonIDs.push_back(i->first);
113   }
114   lock.Leave();
115 
116   CAddonDatabase database;
117   database.Open();
118   for (std::vector<std::string>::iterator it = addonIDs.begin(); it != addonIDs.end(); ++it)
119   {
120     AddonPtr addon;
121     if (database.GetAddon(*it, addon))
122       addons.push_back(addon);
123   }
124 }
125 
GetProgress(const std::string & addonID,unsigned int & percent,bool & downloadFinshed) const126 bool CAddonInstaller::GetProgress(const std::string& addonID, unsigned int& percent, bool& downloadFinshed) const
127 {
128   CSingleLock lock(m_critSection);
129   JobMap::const_iterator i = m_downloadJobs.find(addonID);
130   if (i != m_downloadJobs.end())
131   {
132     percent = i->second.progress;
133     downloadFinshed = i->second.downloadFinshed;
134     return true;
135   }
136   return false;
137 }
138 
Cancel(const std::string & addonID)139 bool CAddonInstaller::Cancel(const std::string &addonID)
140 {
141   CSingleLock lock(m_critSection);
142   JobMap::iterator i = m_downloadJobs.find(addonID);
143   if (i != m_downloadJobs.end())
144   {
145     CJobManager::GetInstance().CancelJob(i->second.jobID);
146     m_downloadJobs.erase(i);
147     if (m_downloadJobs.empty())
148       m_idle.Set();
149     return true;
150   }
151 
152   return false;
153 }
154 
InstallModal(const std::string & addonID,ADDON::AddonPtr & addon,InstallModalPrompt promptForInstall)155 bool CAddonInstaller::InstallModal(const std::string& addonID,
156                                    ADDON::AddonPtr& addon,
157                                    InstallModalPrompt promptForInstall)
158 {
159   if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
160     return false;
161 
162   // we assume that addons that are enabled don't get to this routine (i.e. that GetAddon() has been called)
163   if (CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, ADDON_UNKNOWN, OnlyEnabled::NO))
164     return false; // addon is installed but disabled, and the user has specifically activated something that needs
165                   // the addon - should we enable it?
166 
167   // check we have it available
168   CAddonDatabase database;
169   database.Open();
170   if (!database.GetAddon(addonID, addon))
171     return false;
172 
173   // if specified ask the user if he wants it installed
174   if (promptForInstall == InstallModalPrompt::PROMPT)
175   {
176     if (HELPERS::ShowYesNoDialogLines(CVariant{24076}, CVariant{24100}, CVariant{addon->Name()}, CVariant{24101}) !=
177       DialogResponse::YES)
178     {
179       return false;
180     }
181   }
182 
183   if (!InstallOrUpdate(addonID, BackgroundJob::NO, ModalJob::YES))
184     return false;
185 
186   return CServiceBroker::GetAddonMgr().GetAddon(addonID, addon, ADDON_UNKNOWN, OnlyEnabled::YES);
187 }
188 
189 
InstallOrUpdate(const std::string & addonID,BackgroundJob background,ModalJob modal)190 bool CAddonInstaller::InstallOrUpdate(const std::string& addonID,
191                                       BackgroundJob background,
192                                       ModalJob modal)
193 {
194   AddonPtr addon;
195   RepositoryPtr repo;
196   if (!CAddonInstallJob::GetAddon(addonID, repo, addon))
197     return false;
198 
199   return DoInstall(addon, repo, background, modal, AutoUpdateJob::NO, DependencyJob::NO,
200                    AllowCheckForUpdates::YES);
201 }
202 
InstallOrUpdateDependency(const ADDON::AddonPtr & dependsId,const ADDON::RepositoryPtr & repo)203 bool CAddonInstaller::InstallOrUpdateDependency(const ADDON::AddonPtr& dependsId,
204                                                 const ADDON::RepositoryPtr& repo)
205 {
206   return DoInstall(dependsId, repo, BackgroundJob::NO, ModalJob::NO, AutoUpdateJob::NO,
207                    DependencyJob::YES, AllowCheckForUpdates::YES);
208 }
209 
Install(const std::string & addonId,const AddonVersion & version,const std::string & repoId)210 bool CAddonInstaller::Install(const std::string& addonId,
211                               const AddonVersion& version,
212                               const std::string& repoId)
213 {
214   CLog::Log(LOGDEBUG, "CAddonInstaller: installing '%s' version '%s' from repository '%s'",
215       addonId.c_str(), version.asString().c_str(), repoId.c_str());
216 
217   AddonPtr addon;
218   CAddonDatabase database;
219 
220   if (!database.Open() || !database.GetAddon(addonId, version, repoId, addon))
221     return false;
222 
223   AddonPtr repo;
224   if (!CServiceBroker::GetAddonMgr().GetAddon(repoId, repo, ADDON_REPOSITORY, OnlyEnabled::YES))
225     return false;
226 
227   return DoInstall(addon, std::static_pointer_cast<CRepository>(repo), BackgroundJob::YES,
228                    ModalJob::NO, AutoUpdateJob::NO, DependencyJob::NO, AllowCheckForUpdates::YES);
229 }
230 
DoInstall(const AddonPtr & addon,const RepositoryPtr & repo,BackgroundJob background,ModalJob modal,AutoUpdateJob autoUpdate,DependencyJob dependsInstall,AllowCheckForUpdates allowCheckForUpdates)231 bool CAddonInstaller::DoInstall(const AddonPtr& addon,
232                                 const RepositoryPtr& repo,
233                                 BackgroundJob background,
234                                 ModalJob modal,
235                                 AutoUpdateJob autoUpdate,
236                                 DependencyJob dependsInstall,
237                                 AllowCheckForUpdates allowCheckForUpdates)
238 {
239   // check whether we already have the addon installing
240   CSingleLock lock(m_critSection);
241   if (m_downloadJobs.find(addon->ID()) != m_downloadJobs.end())
242     return false;
243 
244   CAddonInstallJob* installJob = new CAddonInstallJob(addon, repo, autoUpdate);
245   if (background == BackgroundJob::YES)
246   {
247     // Workaround: because CAddonInstallJob is blocking waiting for other jobs, it needs to be run
248     // with priority dedicated.
249     unsigned int jobID = CJobManager::GetInstance().AddJob(installJob, this, CJob::PRIORITY_DEDICATED);
250     m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(jobID)));
251     m_idle.Reset();
252 
253     return true;
254   }
255 
256   m_downloadJobs.insert(make_pair(addon->ID(), CDownloadJob(0)));
257   m_idle.Reset();
258   lock.Leave();
259 
260   installJob->SetDependsInstall(dependsInstall);
261   installJob->SetAllowCheckForUpdates(allowCheckForUpdates);
262 
263   bool result = false;
264   if (modal == ModalJob::YES)
265     result = installJob->DoModal();
266   else
267     result = installJob->DoWork();
268   delete installJob;
269 
270   lock.Enter();
271   JobMap::iterator i = m_downloadJobs.find(addon->ID());
272   m_downloadJobs.erase(i);
273   if (m_downloadJobs.empty())
274     m_idle.Set();
275 
276   return result;
277 }
278 
InstallFromZip(const std::string & path)279 bool CAddonInstaller::InstallFromZip(const std::string &path)
280 {
281   if (!g_passwordManager.CheckMenuLock(WINDOW_ADDON_BROWSER))
282     return false;
283 
284   CLog::Log(LOGDEBUG, "CAddonInstaller: installing from zip '%s'", CURL::GetRedacted(path).c_str());
285 
286   // grab the descriptive XML document from the zip, and read it in
287   CFileItemList items;
288   //! @bug some zip files return a single item (root folder) that we think is stored, so we don't use the zip:// protocol
289   CURL pathToUrl(path);
290   CURL zipDir = URIUtils::CreateArchivePath("zip", pathToUrl, "");
291   if (!CDirectory::GetDirectory(zipDir, items, "", DIR_FLAG_DEFAULTS) ||
292       items.Size() != 1 || !items[0]->m_bIsFolder)
293   {
294     CServiceBroker::GetEventLog().AddWithNotification(EventPtr(new CNotificationEvent(24045,
295         StringUtils::Format(g_localizeStrings.Get(24143).c_str(), path.c_str()),
296         "special://xbmc/media/icon256x256.png", EventLevel::Error)));
297     return false;
298   }
299 
300   AddonPtr addon;
301   if (CServiceBroker::GetAddonMgr().LoadAddonDescription(items[0]->GetPath(), addon))
302     return DoInstall(addon, RepositoryPtr(), BackgroundJob::YES, ModalJob::NO, AutoUpdateJob::NO,
303                      DependencyJob::NO, AllowCheckForUpdates::YES);
304 
305   CServiceBroker::GetEventLog().AddWithNotification(EventPtr(new CNotificationEvent(24045,
306       StringUtils::Format(g_localizeStrings.Get(24143).c_str(), path.c_str()),
307       "special://xbmc/media/icon256x256.png", EventLevel::Error)));
308   return false;
309 }
310 
CheckDependencies(const AddonPtr & addon,CAddonDatabase * database)311 bool CAddonInstaller::CheckDependencies(const AddonPtr &addon, CAddonDatabase *database /* = NULL */)
312 {
313   std::pair<std::string, std::string> failedDep;
314   return CheckDependencies(addon, failedDep, database);
315 }
316 
CheckDependencies(const AddonPtr & addon,std::pair<std::string,std::string> & failedDep,CAddonDatabase * database)317 bool CAddonInstaller::CheckDependencies(const AddonPtr &addon, std::pair<std::string, std::string> &failedDep, CAddonDatabase *database /* = NULL */)
318 {
319   std::vector<std::string> preDeps;
320   preDeps.push_back(addon->ID());
321   CAddonDatabase localDB;
322   if (!database)
323     database = &localDB;
324 
325   return CheckDependencies(addon, preDeps, *database, failedDep);
326 }
327 
CheckDependencies(const AddonPtr & addon,std::vector<std::string> & preDeps,CAddonDatabase & database,std::pair<std::string,std::string> & failedDep)328 bool CAddonInstaller::CheckDependencies(const AddonPtr &addon,
329                                         std::vector<std::string>& preDeps, CAddonDatabase &database,
330                                         std::pair<std::string, std::string> &failedDep)
331 {
332   if (addon == NULL)
333     return true; // a NULL addon has no dependencies
334 
335   if (!database.Open())
336     return false;
337 
338   for (const auto& it : addon->GetDependencies())
339   {
340     const std::string &addonID = it.id;
341     const AddonVersion& versionMin = it.versionMin;
342     const AddonVersion& version = it.version;
343     bool optional = it.optional;
344     AddonPtr dep;
345     bool haveInstalledAddon =
346         CServiceBroker::GetAddonMgr().GetAddon(addonID, dep, ADDON_UNKNOWN, OnlyEnabled::NO);
347     if ((haveInstalledAddon && !dep->MeetsVersion(versionMin, version)) ||
348         (!haveInstalledAddon && !optional))
349     {
350       // we have it but our version isn't good enough, or we don't have it and we need it
351       if (!database.GetAddon(addonID, dep) || !dep->MeetsVersion(versionMin, version))
352       {
353         // we don't have it in a repo, or we have it but the version isn't good enough, so dep isn't satisfied.
354         CLog::Log(LOGDEBUG, "CAddonInstallJob[%s]: requires %s version %s which is not available", addon->ID().c_str(), addonID.c_str(), version.asString().c_str());
355         database.Close();
356 
357         // fill in the details of the failed dependency
358         failedDep.first = addonID;
359         failedDep.second = version.asString();
360 
361         return false;
362       }
363     }
364 
365     // need to enable the dependency
366     if (dep && CServiceBroker::GetAddonMgr().IsAddonDisabled(addonID))
367       if (!CServiceBroker::GetAddonMgr().EnableAddon(addonID))
368       {
369         database.Close();
370         return false;
371       }
372 
373     // 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
374     //! @todo should we assume that installed deps are OK?
375     if (dep && std::find(preDeps.begin(), preDeps.end(), dep->ID()) == preDeps.end())
376     {
377       preDeps.push_back(dep->ID());
378       if (!CheckDependencies(dep, preDeps, database, failedDep))
379       {
380         database.Close();
381         return false;
382       }
383     }
384   }
385   database.Close();
386 
387   return true;
388 }
389 
HasJob(const std::string & ID) const390 bool CAddonInstaller::HasJob(const std::string& ID) const
391 {
392   CSingleLock lock(m_critSection);
393   return m_downloadJobs.find(ID) != m_downloadJobs.end();
394 }
395 
PrunePackageCache()396 void CAddonInstaller::PrunePackageCache()
397 {
398   std::map<std::string, std::unique_ptr<CFileItemList>> packs;
399   int64_t size = EnumeratePackageFolder(packs);
400   int64_t limit = static_cast<int64_t>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_addonPackageFolderSize) * 1024 * 1024;
401   if (size < limit)
402     return;
403 
404   // Prune packages
405   // 1. Remove the largest packages, leaving at least 2 for each add-on
406   CFileItemList items;
407   CAddonDatabase db;
408   db.Open();
409   for (auto it = packs.begin(); it != packs.end(); ++it)
410   {
411     it->second->Sort(SortByLabel, SortOrderDescending);
412     for (int j = 2; j < it->second->Size(); j++)
413       items.Add(CFileItemPtr(new CFileItem(*it->second->Get(j))));
414   }
415 
416   items.Sort(SortBySize, SortOrderDescending);
417   int i = 0;
418   while (size > limit && i < items.Size())
419   {
420     size -= items[i]->m_dwSize;
421     db.RemovePackage(items[i]->GetPath());
422     CFileUtils::DeleteItem(items[i++]);
423   }
424 
425   if (size > limit)
426   {
427     // 2. Remove the oldest packages (leaving least 1 for each add-on)
428     items.Clear();
429     for (auto it = packs.begin(); it != packs.end(); ++it)
430     {
431       if (it->second->Size() > 1)
432         items.Add(CFileItemPtr(new CFileItem(*it->second->Get(1))));
433     }
434 
435     items.Sort(SortByDate, SortOrderAscending);
436     i = 0;
437     while (size > limit && i < items.Size())
438     {
439       size -= items[i]->m_dwSize;
440       db.RemovePackage(items[i]->GetPath());
441       CFileUtils::DeleteItem(items[i++]);
442     }
443   }
444 }
445 
InstallAddons(const VECADDONS & addons,bool wait,AllowCheckForUpdates allowCheckForUpdates)446 void CAddonInstaller::InstallAddons(const VECADDONS& addons,
447                                     bool wait,
448                                     AllowCheckForUpdates allowCheckForUpdates)
449 {
450   for (const auto& addon : addons)
451   {
452     AddonPtr toInstall;
453     RepositoryPtr repo;
454     if (CAddonInstallJob::GetAddon(addon->ID(), repo, toInstall))
455       DoInstall(toInstall, repo, BackgroundJob::NO, ModalJob::NO, AutoUpdateJob::YES,
456                 DependencyJob::NO, allowCheckForUpdates);
457   }
458   if (wait)
459   {
460     CSingleLock lock(m_critSection);
461     if (!m_downloadJobs.empty())
462     {
463       m_idle.Reset();
464       lock.Leave();
465       m_idle.Wait();
466     }
467   }
468 }
469 
EnumeratePackageFolder(std::map<std::string,std::unique_ptr<CFileItemList>> & result)470 int64_t CAddonInstaller::EnumeratePackageFolder(
471     std::map<std::string, std::unique_ptr<CFileItemList>>& result)
472 {
473   CFileItemList items;
474   CDirectory::GetDirectory("special://home/addons/packages/",items,".zip",DIR_FLAG_NO_FILE_DIRS);
475   int64_t size = 0;
476   for (int i = 0; i < items.Size(); i++)
477   {
478     if (items[i]->m_bIsFolder)
479       continue;
480 
481     size += items[i]->m_dwSize;
482     std::string pack,dummy;
483     AddonVersion::SplitFileName(pack, dummy, items[i]->GetLabel());
484     if (result.find(pack) == result.end())
485       result[pack] = std::make_unique<CFileItemList>();
486     result[pack]->Add(CFileItemPtr(new CFileItem(*items[i])));
487   }
488 
489   return size;
490 }
491 
CAddonInstallJob(const AddonPtr & addon,const RepositoryPtr & repo,AutoUpdateJob isAutoUpdate)492 CAddonInstallJob::CAddonInstallJob(const AddonPtr& addon,
493                                    const RepositoryPtr& repo,
494                                    AutoUpdateJob isAutoUpdate)
495   : m_addon(addon), m_repo(repo), m_isAutoUpdate(isAutoUpdate)
496 {
497   AddonPtr dummy;
498   m_isUpdate =
499       CServiceBroker::GetAddonMgr().GetAddon(addon->ID(), dummy, ADDON_UNKNOWN, OnlyEnabled::NO);
500 }
501 
GetAddon(const std::string & addonID,RepositoryPtr & repo,ADDON::AddonPtr & addon)502 bool CAddonInstallJob::GetAddon(const std::string& addonID, RepositoryPtr& repo,
503     ADDON::AddonPtr& addon)
504 {
505   if (!CServiceBroker::GetAddonMgr().FindInstallableById(addonID, addon))
506     return false;
507 
508   AddonPtr tmp;
509   if (!CServiceBroker::GetAddonMgr().GetAddon(addon->Origin(), tmp, ADDON_REPOSITORY,
510                                               OnlyEnabled::YES))
511     return false;
512 
513   repo = std::static_pointer_cast<CRepository>(tmp);
514 
515   return true;
516 }
517 
DoWork()518 bool CAddonInstallJob::DoWork()
519 {
520   m_currentType = CAddonInstallJob::TYPE_DOWNLOAD;
521 
522   SetTitle(StringUtils::Format(g_localizeStrings.Get(24057).c_str(), m_addon->Name().c_str()));
523   SetProgress(0);
524 
525   // check whether all the dependencies are available or not
526   SetText(g_localizeStrings.Get(24058));
527   std::pair<std::string, std::string> failedDep;
528   if (!CAddonInstaller::GetInstance().CheckDependencies(m_addon, failedDep))
529   {
530     std::string details = StringUtils::Format(g_localizeStrings.Get(24142).c_str(), failedDep.first.c_str(), failedDep.second.c_str());
531     CLog::Log(LOGERROR, "CAddonInstallJob[%s]: %s", m_addon->ID().c_str(), details.c_str());
532     ReportInstallError(m_addon->ID(), m_addon->ID(), details);
533     return false;
534   }
535 
536   std::string installFrom;
537   {
538     // Addons are installed by downloading the .zip package on the server to the local
539     // packages folder, then extracting from the local .zip package into the addons folder
540     // Both these functions are achieved by "copying" using the vfs.
541 
542     if (!m_repo && URIUtils::HasSlashAtEnd(m_addon->Path()))
543     { // passed in a folder - all we need do is copy it across
544       installFrom = m_addon->Path();
545     }
546     else
547     {
548       std::string path{m_addon->Path()};
549       TypedDigest hash;
550       if (m_repo)
551       {
552         CRepository::ResolveResult resolvedAddon = m_repo->ResolvePathAndHash(m_addon);
553         path = resolvedAddon.location;
554         hash = resolvedAddon.digest;
555         if (path.empty())
556         {
557           CLog::Log(LOGERROR, "CAddonInstallJob[%s]: failed to resolve addon install source path", m_addon->ID().c_str());
558           ReportInstallError(m_addon->ID(), m_addon->ID());
559           return false;
560         }
561       }
562 
563       CAddonDatabase db;
564       if (!db.Open())
565       {
566         CLog::Log(LOGERROR, "CAddonInstallJob[%s]: failed to open database", m_addon->ID().c_str());
567         ReportInstallError(m_addon->ID(), m_addon->ID());
568         return false;
569       }
570 
571       std::string packageOriginalPath, packageFileName;
572       URIUtils::Split(path, packageOriginalPath, packageFileName);
573       // Use ChangeBasePath so the URL is decoded if necessary
574       const std::string packagePath = "special://home/addons/packages/";
575       //!@todo fix design flaw in file copying: We use CFileOperationJob to download the package from the internet
576       // to the local cache. It tries to be "smart" and decode the URL. But it never tells us what the result is,
577       // so if we try for example to download "http://localhost/a+b.zip" the result ends up in "a b.zip".
578       // First bug is that it actually decodes "+", which is not necessary except in query parts. Second bug
579       // is that we cannot know that it does this and what the result is so the package will not be found without
580       // using ChangeBasePath here (which is the same function the copying code uses and performs the translation).
581       std::string package = URIUtils::ChangeBasePath(packageOriginalPath, packageFileName, packagePath);
582 
583       // check that we don't already have a valid copy
584       if (!hash.Empty())
585       {
586         std::string hashExisting;
587         if (db.GetPackageHash(m_addon->ID(), package, hashExisting) && hash.value != hashExisting)
588         {
589           db.RemovePackage(package);
590         }
591         if (CFile::Exists(package))
592         {
593           CFile::Delete(package);
594         }
595       }
596 
597       // zip passed in - download + extract
598       if (!CFile::Exists(package))
599       {
600         if (!DownloadPackage(path, packagePath))
601         {
602           CFile::Delete(package);
603 
604           CLog::Log(LOGERROR, "CAddonInstallJob[%s]: failed to download %s", m_addon->ID().c_str(), package.c_str());
605           ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
606           return false;
607         }
608       }
609 
610       // at this point we have the package - check that it is valid
611       SetText(g_localizeStrings.Get(24077));
612       if (!hash.Empty())
613       {
614         TypedDigest actualHash{hash.type, CUtil::GetFileDigest(package, hash.type)};
615         if (hash != actualHash)
616         {
617           CFile::Delete(package);
618 
619           CLog::Log(LOGERROR, "CAddonInstallJob[{}]: Hash mismatch after download. Expected {}, was {}",
620               m_addon->ID(), hash.value, actualHash.value);
621           ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
622           return false;
623         }
624 
625         db.AddPackage(m_addon->ID(), package, hash.value);
626       }
627 
628       // check if the archive is valid
629       CURL archive = URIUtils::CreateArchivePath("zip", CURL(package), "");
630 
631       CFileItemList archivedFiles;
632       AddonPtr temp;
633       if (!CDirectory::GetDirectory(archive, archivedFiles, "", DIR_FLAG_DEFAULTS) ||
634           archivedFiles.Size() != 1 || !archivedFiles[0]->m_bIsFolder ||
635           !CServiceBroker::GetAddonMgr().LoadAddonDescription(archivedFiles[0]->GetPath(), temp))
636       {
637         CLog::Log(LOGERROR, "CAddonInstallJob[%s]: invalid package %s", m_addon->ID().c_str(), package.c_str());
638         db.RemovePackage(package);
639         CFile::Delete(package);
640         ReportInstallError(m_addon->ID(), URIUtils::GetFileName(package));
641         return false;
642       }
643 
644       installFrom = package;
645     }
646   }
647 
648   m_currentType = CAddonInstallJob::TYPE_INSTALL;
649 
650   // run any pre-install functions
651   ADDON::OnPreInstall(m_addon);
652 
653   if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID()))
654   {
655     CLog::Log(LOGERROR, "CAddonInstallJob[%s]: failed to unload addon.", m_addon->ID().c_str());
656     return false;
657   }
658 
659   // perform install
660   if (!Install(installFrom, m_repo))
661     return false;
662 
663   // Load new installed and if successed replace defined m_addon here with new one
664   if (!CServiceBroker::GetAddonMgr().LoadAddon(m_addon->ID(), m_addon->Origin(),
665                                                m_addon->Version()) ||
666       !CServiceBroker::GetAddonMgr().GetAddon(m_addon->ID(), m_addon, ADDON_UNKNOWN,
667                                               OnlyEnabled::YES))
668   {
669     CLog::Log(LOGERROR, "CAddonInstallJob[%s]: failed to reload addon", m_addon->ID().c_str());
670     return false;
671   }
672 
673   g_localizeStrings.LoadAddonStrings(URIUtils::AddFileToFolder(m_addon->Path(), "resources/language/"),
674       CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOCALE_LANGUAGE), m_addon->ID());
675 
676   ADDON::OnPostInstall(m_addon, m_isUpdate, IsModal());
677 
678   // Write origin to database via addon manager, where this information is up-to-date.
679   // Needed to set origin correctly for new installed addons.
680 
681   std::string origin;
682   if (m_addon->Origin() == ORIGIN_SYSTEM)
683   {
684     origin = ORIGIN_SYSTEM; // keep system add-on origin as ORIGIN_SYSTEM
685   }
686   else if (m_addon->HasMainType(ADDON_REPOSITORY))
687   {
688     origin = m_addon->ID(); // use own id as origin if repository
689 
690     // if a repository is updated during the add-on migration process, we need to skip
691     // calling CheckForUpdates() on the repo to prevent deadlock issues during migration
692 
693     if (m_allowCheckForUpdates == AllowCheckForUpdates::YES)
694     {
695       if (m_isUpdate)
696       {
697         CLog::Log(LOGDEBUG, "ADDONS: repository [{}] updated. now checking for content updates.",
698                   m_addon->ID());
699         CServiceBroker::GetRepositoryUpdater().CheckForUpdates(
700             std::static_pointer_cast<CRepository>(m_addon), false);
701       }
702     }
703     else
704     {
705       CLog::Log(LOGDEBUG, "ADDONS: skipping CheckForUpdates() on repository [{}].", m_addon->ID());
706     }
707   }
708   else if (m_repo)
709   {
710     origin = m_repo->ID(); // use repo id as origin
711   }
712 
713   CServiceBroker::GetAddonMgr().SetAddonOrigin(m_addon->ID(), origin, m_isUpdate);
714 
715   if (m_dependsInstall == DependencyJob::YES)
716   {
717     CLog::Log(LOGDEBUG, "ADDONS: dependency [{}] will not be version checked and unpinned",
718               m_addon->ID());
719   }
720   else
721   {
722     // we only do pinning/unpinning for non-zip installs and not system origin
723     if (!m_addon->Origin().empty() && m_addon->Origin() != ORIGIN_SYSTEM)
724     {
725       std::vector<std::shared_ptr<IAddon>> compatibleVersions;
726 
727       // get all compatible versions of an addon-id regardless of their origin
728       CServiceBroker::GetAddonMgr().GetCompatibleVersions(m_addon->ID(), compatibleVersions);
729 
730       // find the latest version for the origin we installed from
731       AddonVersion latestVersion; // initializes to 0.0.0
732       for (const auto& compatibleVersion : compatibleVersions)
733       {
734         if (compatibleVersion->Origin() == m_addon->Origin() &&
735             compatibleVersion->Version() > latestVersion)
736         {
737           latestVersion = compatibleVersion->Version();
738         }
739       }
740 
741       if (m_addon->Version() == latestVersion)
742       {
743         // unpin the installed addon if it's the latest of its origin
744         CServiceBroker::GetAddonMgr().RemoveUpdateRuleFromList(m_addon->ID(),
745                                                                AddonUpdateRule::PIN_OLD_VERSION);
746         CLog::Log(LOGDEBUG, "ADDONS: unpinned: [{}] Origin: {} Version: {}", m_addon->ID(),
747                   m_addon->Origin(), m_addon->Version().asString());
748       }
749       else
750       {
751         // ..pin if it is NOT the latest
752         CServiceBroker::GetAddonMgr().AddUpdateRuleToList(m_addon->ID(),
753                                                           AddonUpdateRule::PIN_OLD_VERSION);
754         CLog::Log(LOGDEBUG, "ADDONS: pinned: [{}] Origin: {} Version: {}", m_addon->ID(),
755                   m_addon->Origin(), m_addon->Version().asString());
756       }
757     }
758     else
759     {
760       CLog::Log(LOGDEBUG,
761                 "ADDONS: zip installed addon [{}] will not be version checked and unpinned",
762                 m_addon->ID());
763     }
764   }
765 
766   bool notify = (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(
767                      CSettings::SETTING_ADDONS_NOTIFICATIONS) ||
768                  m_isAutoUpdate == AutoUpdateJob::NO) &&
769                 !IsModal() && m_dependsInstall == DependencyJob::NO;
770   CServiceBroker::GetEventLog().Add(
771       EventPtr(new CAddonManagementEvent(m_addon, m_isUpdate ? 24065 : 24084)), notify, false);
772 
773   if (m_isAutoUpdate == AutoUpdateJob::YES &&
774       m_addon->LifecycleState() == AddonLifecycleState::BROKEN)
775   {
776     CLog::Log(LOGDEBUG, "CAddonInstallJob[%s]: auto-disabling due to being marked as broken", m_addon->ID().c_str());
777     CServiceBroker::GetAddonMgr().DisableAddon(m_addon->ID(), AddonDisabledReason::USER);
778     CServiceBroker::GetEventLog().Add(EventPtr(new CAddonManagementEvent(m_addon, 24094)), true, false);
779   }
780   else if (m_addon->LifecycleState() == AddonLifecycleState::DEPRECATED)
781   {
782     CLog::Log(LOGDEBUG, "CAddonInstallJob[%s]: installed addon marked as deprecated",
783               m_addon->ID().c_str());
784     std::string text =
785         StringUtils::Format(g_localizeStrings.Get(24168), m_addon->LifecycleStateDescription());
786     CServiceBroker::GetEventLog().Add(EventPtr(new CAddonManagementEvent(m_addon, text)), true,
787                                       false);
788   }
789 
790   // and we're done!
791   MarkFinished();
792   return true;
793 }
794 
DownloadPackage(const std::string & path,const std::string & dest)795 bool CAddonInstallJob::DownloadPackage(const std::string &path, const std::string &dest)
796 {
797   if (ShouldCancel(0, 1))
798     return false;
799 
800   SetText(g_localizeStrings.Get(24078));
801 
802   // need to download/copy the package first
803   CFileItemList list;
804   list.Add(CFileItemPtr(new CFileItem(path, false)));
805   list[0]->Select(true);
806 
807   return DoFileOperation(CFileOperationJob::ActionReplace, list, dest, true);
808 }
809 
DoFileOperation(FileAction action,CFileItemList & items,const std::string & file,bool useSameJob)810 bool CAddonInstallJob::DoFileOperation(FileAction action, CFileItemList &items, const std::string &file, bool useSameJob /* = true */)
811 {
812   bool result = false;
813   if (useSameJob)
814   {
815     SetFileOperation(action, items, file);
816 
817     // temporarily disable auto-closing so not to close the current progress indicator
818     bool autoClose = GetAutoClose();
819     if (autoClose)
820       SetAutoClose(false);
821     // temporarily disable updating title or text
822     bool updateInformation = GetUpdateInformation();
823     if (updateInformation)
824       SetUpdateInformation(false);
825 
826     result = CFileOperationJob::DoWork();
827 
828     SetUpdateInformation(updateInformation);
829     SetAutoClose(autoClose);
830   }
831   else
832   {
833    CFileOperationJob job(action, items, file);
834 
835    // pass our progress indicators to the temporary job and only allow it to
836    // show progress updates (no title or text changes)
837    job.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), GetUpdateProgress(), false);
838 
839    result = job.DoWork();
840   }
841 
842   return result;
843 }
844 
Install(const std::string & installFrom,const RepositoryPtr & repo)845 bool CAddonInstallJob::Install(const std::string &installFrom, const RepositoryPtr& repo)
846 {
847   const auto& deps = m_addon->GetDependencies();
848 
849   if (!deps.empty() && m_addon->HasType(ADDON_REPOSITORY))
850   {
851     bool notSystemAddon = std::none_of(deps.begin(), deps.end(), [](const DependencyInfo& dep) {
852       return CServiceBroker::GetAddonMgr().IsSystemAddon(dep.id);
853     });
854 
855     if (notSystemAddon)
856     {
857       CLog::Log(
858           LOGERROR,
859           "CAddonInstallJob::{}: failed to install repository [{}]. It has dependencies defined",
860           __func__, m_addon->ID());
861       ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24088));
862       return false;
863     }
864   }
865 
866   SetText(g_localizeStrings.Get(24079));
867   unsigned int totalSteps = static_cast<unsigned int>(deps.size()) + 1;
868   if (ShouldCancel(0, totalSteps))
869     return false;
870 
871   const auto& addonMgr = CServiceBroker::GetAddonMgr();
872   CAddonRepos addonRepos(addonMgr);
873   CAddonDatabase database;
874 
875   if (database.Open())
876   {
877     addonRepos.LoadAddonsFromDatabase(database);
878     database.Close();
879   }
880 
881   // The first thing we do is install dependencies
882   for (auto it = deps.begin(); it != deps.end(); ++it)
883   {
884     if (it->id != "xbmc.metadata")
885     {
886       const std::string &addonID = it->id;
887       const AddonVersion& versionMin = it->versionMin;
888       const AddonVersion& version = it->version;
889       bool optional = it->optional;
890       AddonPtr dependency;
891       bool haveInstalledAddon =
892           addonMgr.GetAddon(addonID, dependency, ADDON_UNKNOWN, OnlyEnabled::NO);
893       if ((haveInstalledAddon && !dependency->MeetsVersion(versionMin, version)) ||
894           (!haveInstalledAddon && !optional))
895       {
896         // we have it but our version isn't good enough, or we don't have it and we need it
897 
898         // dependency is already queued up for install - ::Install will fail
899         // instead we wait until the Job has finished. note that we
900         // recall install on purpose in case prior installation failed
901         if (CAddonInstaller::GetInstance().HasJob(addonID))
902         {
903           while (CAddonInstaller::GetInstance().HasJob(addonID))
904             KODI::TIME::Sleep(50);
905 
906           if (!CServiceBroker::GetAddonMgr().IsAddonInstalled(addonID))
907           {
908             CLog::Log(LOGERROR, "CAddonInstallJob[%s]: failed to install dependency %s", m_addon->ID().c_str(), addonID.c_str());
909             ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
910             return false;
911           }
912         }
913         // don't have the addon or the addon isn't new enough - grab it (no new job for these)
914         else
915         {
916           RepositoryPtr repoForDep;
917           AddonPtr dependencyToInstall;
918 
919           // origin of m_addon is empty at least if an addon is installed for the first time
920           // we need to override "parentRepoId" if the passed in repo is valid.
921 
922           const std::string& parentRepoId =
923               m_addon->Origin().empty() && repo ? repo->ID() : m_addon->Origin();
924 
925           if (!addonRepos.FindDependency(addonID, parentRepoId, dependencyToInstall, repoForDep))
926           {
927             CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to find dependency {}", m_addon->ID(),
928                       addonID);
929             ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
930             return false;
931           }
932           else
933           {
934             if (!dependencyToInstall->MeetsVersion(versionMin, version))
935             {
936               CLog::Log(LOGERROR,
937                         "CAddonInstallJob[{}]: found dependency [{}/{}] doesn't meet minimum "
938                         "version [{}]",
939                         m_addon->ID(), addonID, dependencyToInstall->Version().asString(),
940                         versionMin.asString());
941               ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
942               return false;
943             }
944           }
945 
946           if (IsModal())
947           {
948             CAddonInstallJob dependencyJob(dependencyToInstall, repoForDep, AutoUpdateJob::NO);
949             dependencyJob.SetDependsInstall(DependencyJob::YES);
950 
951             // pass our progress indicators to the temporary job and don't allow it to
952             // show progress or information updates (no progress, title or text changes)
953             dependencyJob.SetProgressIndicators(GetProgressBar(), GetProgressDialog(), false,
954                                                 false);
955 
956             if (!dependencyJob.DoModal())
957             {
958               CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
959                         m_addon->ID(), addonID);
960               ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
961               return false;
962             }
963           }
964           else if (!CAddonInstaller::GetInstance().InstallOrUpdateDependency(dependencyToInstall,
965                                                                              repoForDep))
966           {
967             CLog::Log(LOGERROR, "CAddonInstallJob[{}]: failed to install dependency {}",
968                       m_addon->ID(), dependencyToInstall->ID());
969             ReportInstallError(m_addon->ID(), m_addon->ID(), g_localizeStrings.Get(24085));
970             return false;
971           }
972         }
973       }
974     }
975 
976     if (ShouldCancel(std::distance(deps.begin(), it), totalSteps))
977         return false;
978   }
979 
980   SetText(g_localizeStrings.Get(24086));
981   SetProgress(static_cast<unsigned int>(100.0 * (totalSteps - 1.0) / totalSteps));
982 
983   CFilesystemInstaller fsInstaller;
984   if (!fsInstaller.InstallToFilesystem(installFrom, m_addon->ID()))
985   {
986     ReportInstallError(m_addon->ID(), m_addon->ID());
987     return false;
988   }
989 
990   SetProgress(100);
991 
992   return true;
993 }
994 
ReportInstallError(const std::string & addonID,const std::string & fileName,const std::string & message)995 void CAddonInstallJob::ReportInstallError(const std::string& addonID, const std::string& fileName, const std::string& message /* = "" */)
996 {
997   AddonPtr addon;
998   CAddonDatabase database;
999   if (database.Open())
1000   {
1001     database.GetAddon(addonID, addon);
1002     database.Close();
1003   }
1004 
1005   MarkFinished();
1006 
1007   std::string msg = message;
1008   EventPtr activity;
1009   if (addon != NULL)
1010   {
1011     AddonPtr addon2;
1012     CServiceBroker::GetAddonMgr().GetAddon(addonID, addon2, ADDON_UNKNOWN, OnlyEnabled::YES);
1013     if (msg.empty())
1014       msg = g_localizeStrings.Get(addon2 != NULL ? 113 : 114);
1015 
1016     activity = EventPtr(new CAddonManagementEvent(addon, EventLevel::Error, msg));
1017     if (IsModal())
1018       HELPERS::ShowOKDialogText(CVariant{m_addon->Name()}, CVariant{msg});
1019   }
1020   else
1021   {
1022     activity = EventPtr(new CNotificationEvent(24045,
1023         !msg.empty() ? msg : StringUtils::Format(g_localizeStrings.Get(24143).c_str(), fileName.c_str()),
1024         EventLevel::Error));
1025 
1026     if (IsModal())
1027       HELPERS::ShowOKDialogText(CVariant{fileName}, CVariant{msg});
1028   }
1029 
1030   CServiceBroker::GetEventLog().Add(activity, !IsModal(), false);
1031 }
1032 
CAddonUnInstallJob(const AddonPtr & addon,bool removeData)1033 CAddonUnInstallJob::CAddonUnInstallJob(const AddonPtr &addon, bool removeData)
1034   : m_addon(addon), m_removeData(removeData)
1035 { }
1036 
DoWork()1037 bool CAddonUnInstallJob::DoWork()
1038 {
1039   ADDON::OnPreUnInstall(m_addon);
1040 
1041   //Unregister addon with the manager to ensure nothing tries
1042   //to interact with it while we are uninstalling.
1043   if (!CServiceBroker::GetAddonMgr().UnloadAddon(m_addon->ID()))
1044   {
1045     CLog::Log(LOGERROR, "CAddonUnInstallJob[%s]: failed to unload addon.", m_addon->ID().c_str());
1046     return false;
1047   }
1048 
1049   CFilesystemInstaller fsInstaller;
1050   if (!fsInstaller.UnInstallFromFilesystem(m_addon->Path()))
1051   {
1052     CLog::Log(LOGERROR, "CAddonUnInstallJob[%s]: could not delete addon data.", m_addon->ID().c_str());
1053     return false;
1054   }
1055 
1056   ClearFavourites();
1057   if (m_removeData)
1058   {
1059     CFileUtils::DeleteItem("special://profile/addon_data/"+m_addon->ID()+"/");
1060   }
1061 
1062   AddonPtr addon;
1063   CAddonDatabase database;
1064   // try to get the addon object from the repository as the local one does not exist anymore
1065   // if that doesn't work fall back to the local one
1066   if (!database.Open() || !database.GetAddon(m_addon->ID(), addon) || addon == NULL)
1067     addon = m_addon;
1068   CServiceBroker::GetEventLog().Add(EventPtr(new CAddonManagementEvent(addon, 24144)));
1069 
1070   CServiceBroker::GetAddonMgr().OnPostUnInstall(m_addon->ID());
1071   database.OnPostUnInstall(m_addon->ID());
1072 
1073   ADDON::OnPostUnInstall(m_addon);
1074   return true;
1075 }
1076 
ClearFavourites()1077 void CAddonUnInstallJob::ClearFavourites()
1078 {
1079   bool bSave = false;
1080   CFileItemList items;
1081   CServiceBroker::GetFavouritesService().GetAll(items);
1082   for (int i = 0; i < items.Size(); i++)
1083   {
1084     if (items[i]->GetPath().find(m_addon->ID()) != std::string::npos)
1085     {
1086       items.Remove(items[i].get());
1087       bSave = true;
1088     }
1089   }
1090 
1091   if (bSave)
1092     CServiceBroker::GetFavouritesService().Save(items);
1093 }
1094