/* * Copyright (C) 2005-2013 Team XBMC * http://xbmc.org * * This Program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This Program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with XBMC; see the file COPYING. If not, see * . * */ #include "Repository.h" #include "addons/AddonDatabase.h" #include "addons/AddonInstaller.h" #include "addons/AddonManager.h" #include "dialogs/GUIDialogYesNo.h" #include "dialogs/GUIDialogKaiToast.h" #include "filesystem/File.h" #include "filesystem/PluginDirectory.h" #include "settings/Settings.h" #include "utils/log.h" #include "utils/JobManager.h" #include "utils/StringUtils.h" #include "utils/URIUtils.h" #include "utils/XBMCTinyXML.h" #include "FileItem.h" #include "TextureDatabase.h" #include "URL.h" using namespace std; using namespace XFILE; using namespace ADDON; AddonPtr CRepository::Clone() const { return AddonPtr(new CRepository(*this)); } CRepository::CRepository(const AddonProps& props) : CAddon(props) { } CRepository::CRepository(const cp_extension_t *ext) : CAddon(ext) { // read in the other props that we need if (ext) { AddonVersion version("0.0.0"); AddonPtr addonver; if (CAddonMgr::Get().GetAddon("xbmc.addon", addonver)) version = addonver->Version(); for (size_t i = 0; i < ext->configuration->num_children; ++i) { if(ext->configuration->children[i].name && strcmp(ext->configuration->children[i].name, "dir") == 0) { AddonVersion min_version(CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "@minversion")); if (min_version <= version) { DirInfo dir; dir.version = min_version; dir.checksum = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "checksum"); dir.compressed = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "info@compressed") == "true"; dir.info = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "info"); dir.datadir = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "datadir"); dir.zipped = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "datadir@zip") == "true"; dir.hashes = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "hashes") == "true"; m_dirs.push_back(dir); } } } // backward compatibility if (!CAddonMgr::Get().GetExtValue(ext->configuration, "info").empty()) { DirInfo info; info.checksum = CAddonMgr::Get().GetExtValue(ext->configuration, "checksum"); info.compressed = CAddonMgr::Get().GetExtValue(ext->configuration, "info@compressed") == "true"; info.info = CAddonMgr::Get().GetExtValue(ext->configuration, "info"); info.datadir = CAddonMgr::Get().GetExtValue(ext->configuration, "datadir"); info.zipped = CAddonMgr::Get().GetExtValue(ext->configuration, "datadir@zip") == "true"; info.hashes = CAddonMgr::Get().GetExtValue(ext->configuration, "hashes") == "true"; m_dirs.push_back(info); } } } CRepository::CRepository(const CRepository &rhs) : CAddon(rhs), m_dirs(rhs.m_dirs) { } CRepository::~CRepository() { } string CRepository::FetchChecksum(const string& url) { CFile file; try { if (file.Open(url)) { // we intentionally avoid using file.GetLength() for // Transfer-Encoding: chunked servers. std::stringstream str; char temp[1024]; int read; while ((read=file.Read(temp, sizeof(temp))) > 0) str.write(temp, read); return str.str(); } return ""; } catch (...) { return ""; } } string CRepository::GetAddonHash(const AddonPtr& addon) const { string checksum; DirList::const_iterator it; for (it = m_dirs.begin();it != m_dirs.end(); ++it) if (URIUtils::IsInPath(addon->Path(), it->datadir)) break; if (it != m_dirs.end() && it->hashes) { checksum = FetchChecksum(addon->Path()+".md5"); size_t pos = checksum.find_first_of(" \n"); if (pos != string::npos) return checksum.substr(0, pos); } return checksum; } #define SET_IF_NOT_EMPTY(x,y) \ { \ if (!x.empty()) \ x = y; \ } bool CRepository::Parse(const DirInfo& dir, VECADDONS &result) { string file = dir.info; if (dir.compressed) { CURL url(dir.info); string opts = url.GetProtocolOptions(); if (!opts.empty()) opts += "&"; url.SetProtocolOptions(opts+"Encoding=gzip"); file = url.Get(); } CXBMCTinyXML doc; if (doc.LoadFile(file) && doc.RootElement() && CAddonMgr::Get().AddonsFromRepoXML(doc.RootElement(), result)) { for (IVECADDONS i = result.begin(); i != result.end(); ++i) { AddonPtr addon = *i; if (dir.zipped) { string file = StringUtils::Format("%s/%s-%s.zip", addon->ID().c_str(), addon->ID().c_str(), addon->Version().asString().c_str()); addon->Props().path = URIUtils::AddFileToFolder(dir.datadir,file); SET_IF_NOT_EMPTY(addon->Props().icon,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/icon.png")) file = StringUtils::Format("%s/changelog-%s.txt", addon->ID().c_str(), addon->Version().asString().c_str()); SET_IF_NOT_EMPTY(addon->Props().changelog,URIUtils::AddFileToFolder(dir.datadir,file)) SET_IF_NOT_EMPTY(addon->Props().fanart,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/fanart.jpg")) } else { addon->Props().path = URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/"); SET_IF_NOT_EMPTY(addon->Props().icon,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/icon.png")) SET_IF_NOT_EMPTY(addon->Props().changelog,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/changelog.txt")) SET_IF_NOT_EMPTY(addon->Props().fanart,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/fanart.jpg")) } } return true; } return false; } void CRepository::OnPostInstall(bool restart, bool update, bool modal) { VECADDONS addons; AddonPtr repo(new CRepository(*this)); addons.push_back(repo); CJobManager::GetInstance().AddJob(new CRepositoryUpdateJob(addons), &CAddonInstaller::Get()); } void CRepository::OnPostUnInstall() { CAddonDatabase database; database.Open(); database.DeleteRepository(ID()); } CRepositoryUpdateJob::CRepositoryUpdateJob(const VECADDONS &repos) : m_repos(repos) { } void MergeAddons(map &addons, const VECADDONS &new_addons) { for (VECADDONS::const_iterator it = new_addons.begin(); it != new_addons.end(); ++it) { map::iterator existing = addons.find((*it)->ID()); if (existing != addons.end()) { // already got it - replace if we have a newer version if (existing->second->Version() < (*it)->Version()) existing->second = *it; } else addons.insert(make_pair((*it)->ID(), *it)); } } bool CRepositoryUpdateJob::DoWork() { map addons; for (VECADDONS::const_iterator i = m_repos.begin(); i != m_repos.end(); ++i) { if (ShouldCancel(0, 0)) return false; const RepositoryPtr repo = std::dynamic_pointer_cast(*i); VECADDONS newAddons; if (GrabAddons(repo, newAddons)) MergeAddons(addons, newAddons); } if (addons.empty()) return true; //Nothing to do // check for updates CAddonDatabase database; database.Open(); database.BeginMultipleExecute(); CTextureDatabase textureDB; textureDB.Open(); textureDB.BeginMultipleExecute(); VECADDONS notifications; for (map::const_iterator i = addons.begin(); i != addons.end(); ++i) { // manager told us to feck off if (ShouldCancel(0,0)) break; AddonPtr newAddon = i->second; bool deps_met = CAddonInstaller::Get().CheckDependencies(newAddon, &database); if (!deps_met && newAddon->Props().broken.empty()) newAddon->Props().broken = "DEPSNOTMET"; // invalidate the art associated with this item if (!newAddon->Props().fanart.empty()) textureDB.InvalidateCachedTexture(newAddon->Props().fanart); if (!newAddon->Props().icon.empty()) textureDB.InvalidateCachedTexture(newAddon->Props().icon); AddonPtr addon; CAddonMgr::Get().GetAddon(newAddon->ID(),addon); if (addon && newAddon->Version() > addon->Version() && !database.IsAddonBlacklisted(newAddon->ID(),newAddon->Version().asString()) && deps_met) { if (CSettings::Get().GetInt("general.addonupdates") == AUTO_UPDATES_ON) { string referer; if (URIUtils::IsInternetStream(newAddon->Path())) referer = StringUtils::Format("Referer=%s-%s.zip",addon->ID().c_str(),addon->Version().asString().c_str()); if (newAddon->CanInstall(referer)) CAddonInstaller::Get().Install(addon->ID(), true, referer); } else notifications.push_back(addon); } // Check if we should mark the add-on as broken. We may have a newer version // of this add-on in the database or installed - if so, we keep it unbroken. bool haveNewer = (addon && addon->Version() > newAddon->Version()) || database.GetAddonVersion(newAddon->ID()) > newAddon->Version(); if (!haveNewer) { if (!newAddon->Props().broken.empty()) { if (database.IsAddonBroken(newAddon->ID()).empty()) { std::string line = g_localizeStrings.Get(24096); if (newAddon->Props().broken == "DEPSNOTMET") line = g_localizeStrings.Get(24104); if (addon && CGUIDialogYesNo::ShowAndGetInput(newAddon->Name(), line, g_localizeStrings.Get(24097), "")) CAddonMgr::Get().DisableAddon(newAddon->ID()); } } database.BreakAddon(newAddon->ID(), newAddon->Props().broken); } } database.CommitMultipleExecute(); textureDB.CommitMultipleExecute(); if (!notifications.empty() && CSettings::Get().GetBool("general.addonnotifications")) { if (notifications.size() == 1) CGUIDialogKaiToast::QueueNotification(notifications[0]->Icon(), g_localizeStrings.Get(24061), notifications[0]->Name(),TOAST_DISPLAY_TIME,false,TOAST_DISPLAY_TIME); else CGUIDialogKaiToast::QueueNotification("", g_localizeStrings.Get(24001), g_localizeStrings.Get(24061),TOAST_DISPLAY_TIME,false,TOAST_DISPLAY_TIME); } return true; } bool CRepositoryUpdateJob::GrabAddons(const RepositoryPtr& repo, VECADDONS& addons) { CAddonDatabase database; database.Open(); string oldReposum; if (!database.GetRepoChecksum(repo->ID(), oldReposum)) oldReposum = ""; string reposum; for (CRepository::DirList::const_iterator it = repo->m_dirs.begin(); it != repo->m_dirs.end(); ++it) { if (ShouldCancel(0, 0)) return false; if (!it->checksum.empty()) { const string dirsum = CRepository::FetchChecksum(it->checksum); if (dirsum.empty()) { CLog::Log(LOGERROR, "Failed to fetch checksum for directory listing %s for repository %s. ", (*it).info.c_str(), repo->ID().c_str()); return false; } reposum += dirsum; } } if (oldReposum != reposum || oldReposum.empty()) { map uniqueAddons; for (CRepository::DirList::const_iterator it = repo->m_dirs.begin(); it != repo->m_dirs.end(); ++it) { if (ShouldCancel(0, 0)) return false; VECADDONS addons; if (!CRepository::Parse(*it, addons)) { //TODO: Hash is invalid and should not be saved, but should we fail? //We can still report a partial addon listing. CLog::Log(LOGERROR, "Failed to read directory listing %s for repository %s. ", (*it).info.c_str(), repo->ID().c_str()); return false; } MergeAddons(uniqueAddons, addons); } bool add = true; if (!repo->Props().libname.empty()) { CFileItemList dummy; string s = StringUtils::Format("plugin://%s/?action=update", repo->ID().c_str()); add = CDirectory::GetDirectory(s, dummy); } if (add) { for (map::const_iterator i = uniqueAddons.begin(); i != uniqueAddons.end(); ++i) addons.push_back(i->second); database.AddRepository(repo->ID(), addons, reposum, repo->Version()); } } else { CLog::Log(LOGDEBUG, "Checksum for repository %s not changed.", repo->ID().c_str()); database.GetRepository(repo->ID(), addons); database.SetRepoTimestamp(repo->ID(), CDateTime::GetCurrentDateTime().GetAsDBDateTime(), repo->Version()); } return true; }