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