1 /*
2  *  Copyright (C) 2005-2020 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 "AddonRepos.h"
10 
11 #include "Addon.h"
12 #include "AddonDatabase.h"
13 #include "AddonManager.h"
14 #include "AddonRepoInfo.h"
15 #include "AddonSystemSettings.h"
16 #include "CompileInfo.h"
17 #include "Repository.h"
18 #include "RepositoryUpdater.h"
19 #include "ServiceBroker.h"
20 #include "messaging/helpers/DialogOKHelper.h"
21 #include "utils/StringUtils.h"
22 #include "utils/log.h"
23 
24 #include <algorithm>
25 #include <vector>
26 
27 using namespace ADDON;
28 
29 static std::vector<RepoInfo> officialRepoInfos = CCompileInfo::LoadOfficialRepoInfos();
30 
31 /**********************************************************
32  * CAddonRepos
33  *
34  */
35 
IsFromOfficialRepo(const std::shared_ptr<IAddon> & addon,CheckAddonPath checkAddonPath)36 bool CAddonRepos::IsFromOfficialRepo(const std::shared_ptr<IAddon>& addon,
37                                      CheckAddonPath checkAddonPath)
38 {
39   auto comparator = [&](const RepoInfo& officialRepo) {
40     if (checkAddonPath == CheckAddonPath::YES)
41     {
42       return (addon->Origin() == officialRepo.m_repoId &&
43               StringUtils::StartsWithNoCase(addon->Path(), officialRepo.m_origin));
44     }
45 
46     return addon->Origin() == officialRepo.m_repoId;
47   };
48 
49   return addon->Origin() == ORIGIN_SYSTEM ||
50          std::any_of(officialRepoInfos.begin(), officialRepoInfos.end(), comparator);
51 }
52 
IsOfficialRepo(const std::string & repoId)53 bool CAddonRepos::IsOfficialRepo(const std::string& repoId)
54 {
55   return repoId == ORIGIN_SYSTEM || std::any_of(officialRepoInfos.begin(), officialRepoInfos.end(),
56                                                 [&repoId](const RepoInfo& officialRepo) {
57                                                   return repoId == officialRepo.m_repoId;
58                                                 });
59 }
60 
LoadAddonsFromDatabase(const CAddonDatabase & database)61 bool CAddonRepos::LoadAddonsFromDatabase(const CAddonDatabase& database)
62 {
63   return LoadAddonsFromDatabase(database, "", nullptr);
64 }
65 
LoadAddonsFromDatabase(const CAddonDatabase & database,const std::string & addonId)66 bool CAddonRepos::LoadAddonsFromDatabase(const CAddonDatabase& database, const std::string& addonId)
67 {
68   return LoadAddonsFromDatabase(database, addonId, nullptr);
69 }
70 
LoadAddonsFromDatabase(const CAddonDatabase & database,const std::shared_ptr<IAddon> & repoAddon)71 bool CAddonRepos::LoadAddonsFromDatabase(const CAddonDatabase& database,
72                                          const std::shared_ptr<IAddon>& repoAddon)
73 {
74   return LoadAddonsFromDatabase(database, "", repoAddon);
75 }
76 
LoadAddonsFromDatabase(const CAddonDatabase & database,const std::string & addonId,const std::shared_ptr<IAddon> & repoAddon)77 bool CAddonRepos::LoadAddonsFromDatabase(const CAddonDatabase& database,
78                                          const std::string& addonId,
79                                          const std::shared_ptr<IAddon>& repoAddon)
80 {
81   m_allAddons.clear();
82 
83   if (repoAddon)
84   {
85     if (!database.GetRepositoryContent(repoAddon->ID(), m_allAddons))
86     {
87       // Repo content is invalid. Ask for update and wait.
88       CServiceBroker::GetRepositoryUpdater().CheckForUpdates(
89           std::static_pointer_cast<CRepository>(repoAddon));
90       CServiceBroker::GetRepositoryUpdater().Await();
91 
92       if (!database.GetRepositoryContent(repoAddon->ID(), m_allAddons))
93       {
94         KODI::MESSAGING::HELPERS::ShowOKDialogText(CVariant{repoAddon->Name()}, CVariant{24991});
95         return false;
96       }
97     }
98   }
99   else if (addonId.empty())
100   {
101     // load full repository content
102     database.GetRepositoryContent(m_allAddons);
103   }
104   else
105   {
106     // load specific addonId only
107     database.FindByAddonId(addonId, m_allAddons);
108   }
109 
110   m_addonsByRepoMap.clear();
111   for (const auto& addon : m_allAddons)
112   {
113     if (m_addonMgr.IsCompatible(*addon))
114     {
115       m_addonsByRepoMap[addon->Origin()].insert({addon->ID(), addon});
116     }
117   }
118 
119   for (const auto& map : m_addonsByRepoMap)
120     CLog::Log(LOGDEBUG, "ADDONS: repo: {} - {} addon(s) loaded", map.first, map.second.size());
121 
122   SetupLatestVersionMaps();
123 
124   return true;
125 }
126 
SetupLatestVersionMaps()127 void CAddonRepos::SetupLatestVersionMaps()
128 {
129   m_latestOfficialVersions.clear();
130   m_latestPrivateVersions.clear();
131   m_latestVersionsByRepo.clear();
132 
133   for (const auto& repo : m_addonsByRepoMap)
134   {
135     const auto& addonsPerRepo = repo.second;
136 
137     for (const auto& addonMapEntry : addonsPerRepo)
138     {
139       const auto& addonToAdd = addonMapEntry.second;
140 
141       if (IsFromOfficialRepo(addonToAdd, CheckAddonPath::YES))
142       {
143         AddAddonIfLatest(addonToAdd, m_latestOfficialVersions);
144       }
145       else
146       {
147         AddAddonIfLatest(addonToAdd, m_latestPrivateVersions);
148       }
149 
150       // add to latestVersionsByRepo
151       AddAddonIfLatest(repo.first, addonToAdd, m_latestVersionsByRepo);
152     }
153   }
154 }
155 
AddAddonIfLatest(const std::shared_ptr<IAddon> & addonToAdd,std::map<std::string,std::shared_ptr<IAddon>> & map) const156 void CAddonRepos::AddAddonIfLatest(const std::shared_ptr<IAddon>& addonToAdd,
157                                    std::map<std::string, std::shared_ptr<IAddon>>& map) const
158 {
159   const auto& latestKnown = map.find(addonToAdd->ID());
160   if (latestKnown == map.end() || addonToAdd->Version() > latestKnown->second->Version())
161     map[addonToAdd->ID()] = addonToAdd;
162 }
163 
AddAddonIfLatest(const std::string & repoId,const std::shared_ptr<IAddon> & addonToAdd,std::map<std::string,std::map<std::string,std::shared_ptr<IAddon>>> & map) const164 void CAddonRepos::AddAddonIfLatest(
165     const std::string& repoId,
166     const std::shared_ptr<IAddon>& addonToAdd,
167     std::map<std::string, std::map<std::string, std::shared_ptr<IAddon>>>& map) const
168 {
169   const auto& latestVersionByRepo = map.find(repoId);
170 
171   if (latestVersionByRepo == map.end()) // repo not found
172   {
173     map[repoId].insert({addonToAdd->ID(), addonToAdd});
174   }
175   else
176   {
177     const auto& latestVersionEntryByRepo = latestVersionByRepo->second;
178     const auto& latestKnown = latestVersionEntryByRepo.find(addonToAdd->ID());
179 
180     if (latestKnown == latestVersionEntryByRepo.end() ||
181         addonToAdd->Version() > latestKnown->second->Version())
182       map[repoId][addonToAdd->ID()] = addonToAdd;
183   }
184 }
185 
BuildUpdateOrOutdatedList(const std::vector<std::shared_ptr<IAddon>> & installed,std::vector<std::shared_ptr<IAddon>> & result,AddonCheckType addonCheckType) const186 void CAddonRepos::BuildUpdateOrOutdatedList(const std::vector<std::shared_ptr<IAddon>>& installed,
187                                             std::vector<std::shared_ptr<IAddon>>& result,
188                                             AddonCheckType addonCheckType) const
189 {
190   std::shared_ptr<IAddon> update;
191 
192   CLog::Log(LOGDEBUG, "CAddonRepos::{}: Building {} list from installed add-ons", __func__,
193             addonCheckType == AddonCheckType::OUTDATED_ADDONS ? "outdated" : "update");
194 
195   for (const auto& addon : installed)
196   {
197     if (DoAddonUpdateCheck(addon, update))
198     {
199       result.emplace_back(addonCheckType == AddonCheckType::OUTDATED_ADDONS ? addon : update);
200     }
201   }
202 }
203 
BuildAddonsWithUpdateList(const std::vector<std::shared_ptr<IAddon>> & installed,std::map<std::string,CAddonWithUpdate> & addonsWithUpdate) const204 void CAddonRepos::BuildAddonsWithUpdateList(
205     const std::vector<std::shared_ptr<IAddon>>& installed,
206     std::map<std::string, CAddonWithUpdate>& addonsWithUpdate) const
207 {
208   std::shared_ptr<IAddon> update;
209 
210   CLog::Log(LOGDEBUG,
211             "CAddonRepos::{}: Building combined addons-with-update map from installed add-ons",
212             __func__);
213 
214   for (const auto& addon : installed)
215   {
216     if (DoAddonUpdateCheck(addon, update))
217     {
218       addonsWithUpdate.insert({addon->ID(), {addon, update}});
219     }
220   }
221 }
222 
DoAddonUpdateCheck(const std::shared_ptr<IAddon> & addon,std::shared_ptr<IAddon> & update) const223 bool CAddonRepos::DoAddonUpdateCheck(const std::shared_ptr<IAddon>& addon,
224                                      std::shared_ptr<IAddon>& update) const
225 {
226   CLog::Log(LOGDEBUG, "ADDONS: update check: addonID = {} / Origin = {} / Version = {}",
227             addon->ID(), addon->Origin(), addon->Version().asString());
228 
229   update.reset();
230 
231   const AddonRepoUpdateMode updateMode =
232       CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
233 
234   bool hasOfficialUpdate = FindAddonAndCheckForUpdate(addon, m_latestOfficialVersions, update);
235 
236   // addons with an empty origin have at least been checked against official repositories
237   if (!addon->Origin().empty())
238   {
239     if (ORIGIN_SYSTEM != addon->Origin() && !hasOfficialUpdate) // not a system addon
240     {
241       // If we didn't find an official update
242       if (IsFromOfficialRepo(addon, CheckAddonPath::YES)) // is an official addon
243       {
244         if (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY)
245         {
246           if (!FindAddonAndCheckForUpdate(addon, m_latestPrivateVersions, update))
247           {
248             return false;
249           }
250         }
251       }
252       else
253       {
254         // ...we check for updates in the origin repo only
255         const auto& repoEntry = m_latestVersionsByRepo.find(addon->Origin());
256         if (repoEntry != m_latestVersionsByRepo.end())
257         {
258           if (!FindAddonAndCheckForUpdate(addon, repoEntry->second, update))
259           {
260             return false;
261           }
262         }
263       }
264     }
265   }
266 
267   if (update != nullptr)
268   {
269     CLog::Log(LOGDEBUG, "ADDONS: -- found -->: addonID = {} / Origin = {} / Version = {}",
270               update->ID(), update->Origin(), update->Version().asString());
271     return true;
272   }
273 
274   return false;
275 }
276 
FindAddonAndCheckForUpdate(const std::shared_ptr<IAddon> & addonToCheck,const std::map<std::string,std::shared_ptr<IAddon>> & map,std::shared_ptr<IAddon> & update) const277 bool CAddonRepos::FindAddonAndCheckForUpdate(
278     const std::shared_ptr<IAddon>& addonToCheck,
279     const std::map<std::string, std::shared_ptr<IAddon>>& map,
280     std::shared_ptr<IAddon>& update) const
281 {
282   const auto& remote = map.find(addonToCheck->ID());
283   if (remote != map.end()) // is addon in the desired map?
284   {
285     if ((remote->second->Version() > addonToCheck->Version()) ||
286         m_addonMgr.IsAddonDisabledWithReason(addonToCheck->ID(), AddonDisabledReason::INCOMPATIBLE))
287     {
288       // return addon update
289       update = remote->second;
290     }
291     else
292     {
293       // addon found, but it's up to date
294       update = nullptr;
295     }
296     return true;
297   }
298 
299   return false;
300 }
301 
GetLatestVersionByMap(const std::string & addonId,const std::map<std::string,std::shared_ptr<IAddon>> & map,std::shared_ptr<IAddon> & addon) const302 bool CAddonRepos::GetLatestVersionByMap(const std::string& addonId,
303                                         const std::map<std::string, std::shared_ptr<IAddon>>& map,
304                                         std::shared_ptr<IAddon>& addon) const
305 {
306   const auto& remote = map.find(addonId);
307   if (remote != map.end()) // is addon in the desired map?
308   {
309     addon = remote->second;
310     return true;
311   }
312 
313   return false;
314 }
315 
GetLatestAddonVersionFromAllRepos(const std::string & addonId,std::shared_ptr<IAddon> & addon) const316 bool CAddonRepos::GetLatestAddonVersionFromAllRepos(const std::string& addonId,
317                                                     std::shared_ptr<IAddon>& addon) const
318 {
319   const AddonRepoUpdateMode updateMode =
320       CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
321 
322   bool hasOfficialVersion = GetLatestVersionByMap(addonId, m_latestOfficialVersions, addon);
323 
324   if (hasOfficialVersion)
325   {
326     if (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY)
327     {
328       std::shared_ptr<IAddon> thirdPartyAddon;
329 
330       // only use this version if it's higher than the official one
331       if (GetLatestVersionByMap(addonId, m_latestPrivateVersions, thirdPartyAddon))
332       {
333         if (thirdPartyAddon->Version() > addon->Version())
334           addon = thirdPartyAddon;
335       }
336     }
337   }
338   else
339   {
340     if (!GetLatestVersionByMap(addonId, m_latestPrivateVersions, addon))
341       return false;
342   }
343 
344   return true;
345 }
346 
GetLatestAddonVersions(std::vector<std::shared_ptr<IAddon>> & addonList) const347 void CAddonRepos::GetLatestAddonVersions(std::vector<std::shared_ptr<IAddon>>& addonList) const
348 {
349   const AddonRepoUpdateMode updateMode =
350       CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
351 
352   addonList.clear();
353 
354   // first we insert all official addon versions into the resulting vector
355 
356   std::transform(m_latestOfficialVersions.begin(), m_latestOfficialVersions.end(),
357                  back_inserter(addonList),
358                  [](const std::pair<std::string, std::shared_ptr<IAddon>>& officialVersion) {
359                    return officialVersion.second;
360                  });
361 
362   // then we insert private addon versions if they don't exist in the official map
363   // or installation from ANY_REPOSITORY is allowed and the private version is higher
364 
365   for (const auto& privateVersion : m_latestPrivateVersions)
366   {
367     const auto& officialVersion = m_latestOfficialVersions.find(privateVersion.first);
368     if (officialVersion == m_latestOfficialVersions.end() ||
369         (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY &&
370          privateVersion.second->Version() > officialVersion->second->Version()))
371     {
372       addonList.emplace_back(privateVersion.second);
373     }
374   }
375 }
376 
GetLatestAddonVersionsFromAllRepos(std::vector<std::shared_ptr<IAddon>> & addonList) const377 void CAddonRepos::GetLatestAddonVersionsFromAllRepos(
378     std::vector<std::shared_ptr<IAddon>>& addonList) const
379 {
380   const AddonRepoUpdateMode updateMode =
381       CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
382 
383   addonList.clear();
384 
385   // first we insert all official addon versions into the resulting vector
386 
387   std::transform(m_latestOfficialVersions.begin(), m_latestOfficialVersions.end(),
388                  back_inserter(addonList),
389                  [](const std::pair<std::string, std::shared_ptr<IAddon>>& officialVersion) {
390                    return officialVersion.second;
391                  });
392 
393   // then we insert latest version per addon and repository if they don't exist in the official map
394   // or installation from ANY_REPOSITORY is allowed and the private version is higher
395 
396   for (const auto& repo : m_latestVersionsByRepo)
397   {
398     // content of official repos is stored in m_latestVersionsByRepo too
399     // so we need to filter them out
400 
401     if (std::none_of(officialRepoInfos.begin(), officialRepoInfos.end(),
402                      [&](const ADDON::RepoInfo& officialRepo) {
403                        return repo.first == officialRepo.m_repoId;
404                      }))
405     {
406       for (const auto& latestAddon : repo.second)
407       {
408         const auto& officialVersion = m_latestOfficialVersions.find(latestAddon.first);
409         if (officialVersion == m_latestOfficialVersions.end() ||
410             (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY &&
411              latestAddon.second->Version() > officialVersion->second->Version()))
412         {
413           addonList.emplace_back(latestAddon.second);
414         }
415       }
416     }
417   }
418 }
419 
FindDependency(const std::string & dependsId,const std::string & parentRepoId,std::shared_ptr<IAddon> & dependencyToInstall,std::shared_ptr<CRepository> & repoForDep) const420 bool CAddonRepos::FindDependency(const std::string& dependsId,
421                                  const std::string& parentRepoId,
422                                  std::shared_ptr<IAddon>& dependencyToInstall,
423                                  std::shared_ptr<CRepository>& repoForDep) const
424 {
425   const AddonRepoUpdateMode updateMode =
426       CAddonSystemSettings::GetInstance().GetAddonRepoUpdateMode();
427 
428   bool dependencyHasOfficialVersion =
429       GetLatestVersionByMap(dependsId, m_latestOfficialVersions, dependencyToInstall);
430 
431   if (dependencyHasOfficialVersion)
432   {
433     if (updateMode == AddonRepoUpdateMode::ANY_REPOSITORY)
434     {
435       std::shared_ptr<IAddon> thirdPartyDependency;
436 
437       // only use this version if it's higher than the official one
438       if (GetLatestVersionByMap(dependsId, m_latestPrivateVersions, thirdPartyDependency))
439       {
440         if (thirdPartyDependency->Version() > dependencyToInstall->Version())
441           dependencyToInstall = thirdPartyDependency;
442       }
443     }
444   }
445   else
446   {
447     // If we didn't find an official version of this dependency
448     // ...we check in the origin repo of the parent
449     if (!FindDependencyByParentRepo(dependsId, parentRepoId, dependencyToInstall))
450       return false;
451   }
452 
453   // we got the dependency, so now get a repository-pointer to return
454 
455   std::shared_ptr<IAddon> tmp;
456   if (!m_addonMgr.GetAddon(dependencyToInstall->Origin(), tmp, ADDON_REPOSITORY, OnlyEnabled::YES))
457     return false;
458 
459   repoForDep = std::static_pointer_cast<CRepository>(tmp);
460 
461   CLog::Log(LOGDEBUG, "ADDONS: found dependency [{}] for install/update from repo [{}]",
462             dependencyToInstall->ID(), repoForDep->ID());
463 
464   if (dependencyToInstall->HasType(ADDON_REPOSITORY))
465   {
466     CLog::Log(LOGDEBUG,
467               "ADDONS: dependency with id [{}] has type ADDON_REPOSITORY and will not install!",
468               dependencyToInstall->ID());
469 
470     return false;
471   }
472 
473   return true;
474 }
475 
FindDependencyByParentRepo(const std::string & dependsId,const std::string & parentRepoId,std::shared_ptr<IAddon> & dependencyToInstall) const476 bool CAddonRepos::FindDependencyByParentRepo(const std::string& dependsId,
477                                              const std::string& parentRepoId,
478                                              std::shared_ptr<IAddon>& dependencyToInstall) const
479 {
480   const auto& repoEntry = m_latestVersionsByRepo.find(parentRepoId);
481   if (repoEntry != m_latestVersionsByRepo.end())
482   {
483     if (GetLatestVersionByMap(dependsId, repoEntry->second, dependencyToInstall))
484       return true;
485   }
486 
487   return false;
488 }
489 
BuildCompatibleVersionsList(std::vector<std::shared_ptr<IAddon>> & compatibleVersions) const490 void CAddonRepos::BuildCompatibleVersionsList(
491     std::vector<std::shared_ptr<IAddon>>& compatibleVersions) const
492 {
493   std::vector<std::shared_ptr<IAddon>> officialVersions;
494   std::vector<std::shared_ptr<IAddon>> privateVersions;
495 
496   for (const auto& addon : m_allAddons)
497   {
498     if (m_addonMgr.IsCompatible(*addon))
499     {
500       if (IsFromOfficialRepo(addon, CheckAddonPath::YES))
501       {
502         officialVersions.emplace_back(addon);
503       }
504       else
505       {
506         privateVersions.emplace_back(addon);
507       }
508     }
509   }
510 
511   auto comparator = [](const std::shared_ptr<IAddon>& a, const std::shared_ptr<IAddon>& b) {
512     return (a->Version() > b->Version());
513   };
514 
515   std::sort(officialVersions.begin(), officialVersions.end(), comparator);
516   std::sort(privateVersions.begin(), privateVersions.end(), comparator);
517 
518   compatibleVersions = officialVersions;
519   std::copy(privateVersions.begin(), privateVersions.end(), back_inserter(compatibleVersions));
520 }
521