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