[videodb] remove unused seasons table from episode_view
[xbmc.git] / xbmc / addons / Repository.cpp
blobec9704634e6cbede925205844e620283fc38aa69
1 /*
2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
9 #include "Repository.h"
11 #include "FileItem.h"
12 #include "ServiceBroker.h"
13 #include "URL.h"
14 #include "addons/AddonDatabase.h"
15 #include "addons/AddonInstaller.h"
16 #include "addons/AddonManager.h"
17 #include "addons/RepositoryUpdater.h"
18 #include "addons/addoninfo/AddonInfo.h"
19 #include "addons/addoninfo/AddonType.h"
20 #include "filesystem/CurlFile.h"
21 #include "filesystem/File.h"
22 #include "filesystem/ZipFile.h"
23 #include "messaging/helpers/DialogHelper.h"
24 #include "utils/Base64.h"
25 #include "utils/Digest.h"
26 #include "utils/Mime.h"
27 #include "utils/StringUtils.h"
28 #include "utils/URIUtils.h"
29 #include "utils/log.h"
31 #include <algorithm>
32 #include <iterator>
33 #include <tuple>
34 #include <utility>
36 using namespace XFILE;
37 using namespace ADDON;
38 using namespace KODI::MESSAGING;
39 using KODI::UTILITY::CDigest;
40 using KODI::UTILITY::TypedDigest;
43 CRepository::ResolveResult CRepository::ResolvePathAndHash(const AddonPtr& addon) const
45 std::string const& path = addon->Path();
47 auto dirIt = std::find_if(m_dirs.begin(), m_dirs.end(), [&path](RepositoryDirInfo const& dir) {
48 return URIUtils::PathHasParent(path, dir.datadir, true);
49 });
50 if (dirIt == m_dirs.end())
52 CLog::Log(LOGERROR, "Requested path {} not found in known repository directories", path);
53 return {};
56 if (dirIt->hashType == CDigest::Type::INVALID)
58 // We have a path, but need no hash
59 return {path, {}};
62 // Do not follow mirror redirect, we want the headers of the redirect response
63 CURL url{path};
64 url.SetProtocolOption("redirect-limit", "0");
65 CCurlFile file;
66 if (!file.Open(url))
68 CLog::Log(LOGERROR, "Could not fetch addon location and hash from {}", path);
69 return {};
72 std::string hashTypeStr = CDigest::TypeToString(dirIt->hashType);
74 // Return the location from the header so we don't have to look it up again
75 // (saves one request per addon install)
76 std::string location = file.GetRedirectURL();
77 // content-* headers are base64, convert to base16
78 TypedDigest hash{dirIt->hashType, StringUtils::ToHexadecimal(Base64::Decode(file.GetHttpHeader().GetValue(std::string("content-") + hashTypeStr)))};
80 if (hash.Empty())
82 int tmp;
83 // Expected hash, but none found -> fall back to old method
84 if (!FetchChecksum(path + "." + hashTypeStr, hash.value, tmp) || hash.Empty())
86 CLog::Log(LOGERROR, "Failed to find hash for {} from HTTP header and in separate file", path);
87 return {};
90 if (location.empty())
92 // Fall back to original URL if we do not get a redirect
93 location = path;
96 CLog::Log(LOGDEBUG, "Resolved addon path {} to {} hash {}", path, location, hash.value);
98 return {location, hash};
101 CRepository::CRepository(const AddonInfoPtr& addonInfo) : CAddon(addonInfo, AddonType::REPOSITORY)
103 RepositoryDirList dirs;
104 CAddonVersion version;
105 AddonInfoPtr addonver =
106 CServiceBroker::GetAddonMgr().GetAddonInfo("xbmc.addon", AddonType::UNKNOWN);
107 if (addonver)
108 version = addonver->Version();
110 for (const auto& element : Type(AddonType::REPOSITORY)->GetElements("dir"))
112 RepositoryDirInfo dir = ParseDirConfiguration(element.second);
113 if ((dir.minversion.empty() || version >= dir.minversion) &&
114 (dir.maxversion.empty() || version <= dir.maxversion))
115 m_dirs.push_back(std::move(dir));
118 // old (dharma compatible) way of defining the addon repository structure, is no longer supported
119 // we error out so the user knows how to migrate. The <dir> way is supported since gotham.
120 //! @todo remove if block completely in v21
121 if (!Type(AddonType::REPOSITORY)->GetValue("info").empty())
123 CLog::Log(LOGERROR,
124 "Repository add-on {} uses old schema definition for the repository extension point! "
125 "This is no longer supported, please update your addon to use <dir> definitions.",
126 ID());
129 if (m_dirs.empty())
131 CLog::Log(LOGERROR,
132 "Repository add-on {} does not have any directory matching {} and won't be able to "
133 "update/serve addons! Please fix the addon.xml definition",
134 ID(), version.asString());
137 for (auto const& dir : m_dirs)
139 CURL datadir(dir.datadir);
140 if (datadir.IsProtocol("http"))
142 CLog::Log(LOGWARNING, "Repository add-on {} uses plain HTTP for add-on downloads in path {} - this is insecure and will make your Kodi installation vulnerable to attacks if enabled!", ID(), datadir.GetRedacted());
144 else if (datadir.IsProtocol("https") && datadir.HasProtocolOption("verifypeer") && datadir.GetProtocolOption("verifypeer") == "false")
146 CLog::Log(LOGWARNING, "Repository add-on {} disabled peer verification for add-on downloads in path {} - this is insecure and will make your Kodi installation vulnerable to attacks if enabled!", ID(), datadir.GetRedacted());
151 bool CRepository::FetchChecksum(const std::string& url,
152 std::string& checksum,
153 int& recheckAfter) noexcept
155 CFile file;
156 if (!file.Open(url))
157 return false;
159 // we intentionally avoid using file.GetLength() for
160 // Transfer-Encoding: chunked servers.
161 std::stringstream ss;
162 char temp[1024];
163 int read;
164 while ((read = file.Read(temp, sizeof(temp))) > 0)
165 ss.write(temp, read);
166 if (read <= -1)
167 return false;
168 checksum = ss.str();
169 std::size_t pos = checksum.find_first_of(" \n");
170 if (pos != std::string::npos)
172 checksum.resize(pos);
175 // Determine update interval from (potential) HTTP response
176 // Default: 24 h
177 recheckAfter = 24 * 60 * 60;
178 // This special header is set by the Kodi mirror redirector to control client update frequency
179 // depending on the load on the mirrors
180 const std::string recheckAfterHeader{
181 file.GetProperty(FILE_PROPERTY_RESPONSE_HEADER, "X-Kodi-Recheck-After")};
182 if (!recheckAfterHeader.empty())
186 // Limit value range to sensible values (1 hour to 1 week)
187 recheckAfter =
188 std::max(std::min(std::stoi(recheckAfterHeader), 24 * 7 * 60 * 60), 1 * 60 * 60);
190 catch (...)
192 CLog::Log(LOGWARNING, "Could not parse X-Kodi-Recheck-After header value '{}' from {}",
193 recheckAfterHeader, url);
197 return true;
200 bool CRepository::FetchIndex(const RepositoryDirInfo& repo,
201 std::string const& digest,
202 std::vector<AddonInfoPtr>& addons) noexcept
204 XFILE::CCurlFile http;
206 std::string response;
207 if (!http.Get(repo.info, response))
209 CLog::Log(LOGERROR, "CRepository: failed to read {}", repo.info);
210 return false;
213 if (repo.checksumType != CDigest::Type::INVALID)
215 std::string actualDigest = CDigest::Calculate(repo.checksumType, response);
216 if (!StringUtils::EqualsNoCase(digest, actualDigest))
218 CLog::Log(LOGERROR, "CRepository: {} index has wrong digest {}, expected: {}", repo.info, actualDigest, digest);
219 return false;
223 if (URIUtils::HasExtension(repo.info, ".gz")
224 || CMime::GetFileTypeFromMime(http.GetProperty(XFILE::FILE_PROPERTY_MIME_TYPE)) == CMime::EFileType::FileTypeGZip)
226 CLog::Log(LOGDEBUG, "CRepository '{}' is gzip. decompressing", repo.info);
227 std::string buffer;
228 if (!CZipFile::DecompressGzip(response, buffer))
230 CLog::Log(LOGERROR, "CRepository: failed to decompress gzip from '{}'", repo.info);
231 return false;
233 response = std::move(buffer);
236 return CServiceBroker::GetAddonMgr().AddonsFromRepoXML(repo, response, addons);
239 CRepository::FetchStatus CRepository::FetchIfChanged(const std::string& oldChecksum,
240 std::string& checksum,
241 std::vector<AddonInfoPtr>& addons,
242 int& recheckAfter) const
244 checksum = "";
245 std::vector<std::tuple<RepositoryDirInfo const&, std::string>> dirChecksums;
246 std::vector<int> recheckAfterTimes;
248 for (const auto& dir : m_dirs)
250 if (!dir.checksum.empty())
252 std::string part;
253 int recheckAfterThisDir;
254 if (!FetchChecksum(dir.checksum, part, recheckAfterThisDir))
256 recheckAfter = 1 * 60 * 60; // retry after 1 hour
257 CLog::Log(LOGERROR, "CRepository: failed read '{}'", dir.checksum);
258 return STATUS_ERROR;
260 dirChecksums.emplace_back(dir, part);
261 recheckAfterTimes.push_back(recheckAfterThisDir);
262 checksum += part;
266 // Default interval: 24 h
267 recheckAfter = 24 * 60 * 60;
268 if (dirChecksums.size() == m_dirs.size() && !dirChecksums.empty())
270 // Use smallest update interval out of all received (individual intervals per directory are
271 // not possible)
272 recheckAfter = *std::min_element(recheckAfterTimes.begin(), recheckAfterTimes.end());
273 // If all directories have checksums and they match the last one, nothing has changed
274 if (dirChecksums.size() == m_dirs.size() && oldChecksum == checksum)
275 return STATUS_NOT_MODIFIED;
278 for (const auto& dirTuple : dirChecksums)
280 std::vector<AddonInfoPtr> tmp;
281 if (!FetchIndex(std::get<0>(dirTuple), std::get<1>(dirTuple), tmp))
282 return STATUS_ERROR;
283 addons.insert(addons.end(), tmp.begin(), tmp.end());
285 return STATUS_OK;
288 RepositoryDirInfo CRepository::ParseDirConfiguration(const CAddonExtensions& configuration)
290 RepositoryDirInfo dir;
291 dir.checksum = configuration.GetValue("checksum").asString();
292 std::string checksumStr = configuration.GetValue("checksum@verify").asString();
293 if (!checksumStr.empty())
295 dir.checksumType = CDigest::TypeFromString(checksumStr);
297 dir.info = configuration.GetValue("info").asString();
298 dir.datadir = configuration.GetValue("datadir").asString();
299 dir.artdir = configuration.GetValue("artdir").asString();
300 if (dir.artdir.empty())
302 dir.artdir = dir.datadir;
305 std::string hashStr = configuration.GetValue("hashes").asString();
306 StringUtils::ToLower(hashStr);
307 if (hashStr == "true")
309 // Deprecated alias
310 hashStr = "md5";
312 if (!hashStr.empty() && hashStr != "false")
314 dir.hashType = CDigest::TypeFromString(hashStr);
315 if (dir.hashType == CDigest::Type::MD5)
317 CLog::Log(LOGWARNING, "CRepository::{}: Repository has MD5 hashes enabled - this hash function is broken and will only guard against unintentional data corruption", __FUNCTION__);
321 dir.minversion = CAddonVersion{configuration.GetValue("@minversion").asString()};
322 dir.maxversion = CAddonVersion{configuration.GetValue("@maxversion").asString()};
324 return dir;