Merge pull request #5039 from CEikermann/patch-1
[vuplus_xbmc] / xbmc / addons / Repository.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21 #include "Repository.h"
22 #include "addons/AddonDatabase.h"
23 #include "addons/AddonInstaller.h"
24 #include "addons/AddonManager.h"
25 #include "dialogs/GUIDialogYesNo.h"
26 #include "dialogs/GUIDialogKaiToast.h"
27 #include "filesystem/File.h"
28 #include "filesystem/PluginDirectory.h"
29 #include "pvr/PVRManager.h"
30 #include "settings/Settings.h"
31 #include "utils/log.h"
32 #include "utils/StringUtils.h"
33 #include "utils/URIUtils.h"
34 #include "utils/XBMCTinyXML.h"
35 #include "FileItem.h"
36 #include "TextureDatabase.h"
37 #include "URL.h"
38
39 using namespace std;
40 using namespace XFILE;
41 using namespace ADDON;
42
43 AddonPtr CRepository::Clone() const
44 {
45   return AddonPtr(new CRepository(*this));
46 }
47
48 CRepository::CRepository(const AddonProps& props) :
49   CAddon(props)
50 {
51 }
52
53 CRepository::CRepository(const cp_extension_t *ext)
54   : CAddon(ext)
55 {
56   // read in the other props that we need
57   if (ext)
58   {
59     AddonVersion version("0.0.0");
60     AddonPtr addonver;
61     if (CAddonMgr::Get().GetAddon("xbmc.addon", addonver))
62       version = addonver->Version();
63     for (size_t i = 0; i < ext->configuration->num_children; ++i)
64     {
65       if(ext->configuration->children[i].name &&
66          strcmp(ext->configuration->children[i].name, "dir") == 0)
67       {
68         AddonVersion min_version(CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "@minversion"));
69         if (min_version <= version)
70         {
71           DirInfo dir;
72           dir.version    = min_version;
73           dir.checksum   = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "checksum");
74           dir.compressed = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "info@compressed").Equals("true");
75           dir.info       = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "info");
76           dir.datadir    = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "datadir");
77           dir.zipped     = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "datadir@zip").Equals("true");
78           dir.hashes     = CAddonMgr::Get().GetExtValue(&ext->configuration->children[i], "hashes").Equals("true");
79           m_dirs.push_back(dir);
80         }
81       }
82     }
83     // backward compatibility
84     if (!CAddonMgr::Get().GetExtValue(ext->configuration, "info").empty())
85     {
86       DirInfo info;
87       info.checksum   = CAddonMgr::Get().GetExtValue(ext->configuration, "checksum");
88       info.compressed = CAddonMgr::Get().GetExtValue(ext->configuration, "info@compressed").Equals("true");
89       info.info       = CAddonMgr::Get().GetExtValue(ext->configuration, "info");
90       info.datadir    = CAddonMgr::Get().GetExtValue(ext->configuration, "datadir");
91       info.zipped     = CAddonMgr::Get().GetExtValue(ext->configuration, "datadir@zip").Equals("true");
92       info.hashes     = CAddonMgr::Get().GetExtValue(ext->configuration, "hashes").Equals("true");
93       m_dirs.push_back(info);
94     }
95   }
96 }
97
98 CRepository::CRepository(const CRepository &rhs)
99   : CAddon(rhs)
100 {
101   m_dirs = rhs.m_dirs;
102 }
103
104 CRepository::~CRepository()
105 {
106 }
107
108 string CRepository::Checksum() const
109 {
110   /* This code is duplicated in CRepositoryUpdateJob::GrabAddons().
111    * If you make changes here, they may be applicable there, too.
112    */
113   string result;
114   for (DirList::const_iterator it  = m_dirs.begin(); it != m_dirs.end(); ++it)
115   {
116     if (!it->checksum.empty())
117       result += FetchChecksum(it->checksum);
118   }
119   return result;
120 }
121
122 string CRepository::FetchChecksum(const string& url)
123 {
124   CFile file;
125   try
126   {
127     if (file.Open(url))
128     {    
129       // we intentionally avoid using file.GetLength() for 
130       // Transfer-Encoding: chunked servers.
131       std::stringstream str;
132       char temp[1024];
133       int read;
134       while ((read=file.Read(temp, sizeof(temp))) > 0)
135         str.write(temp, read);
136       return str.str();
137     }
138     return "";
139   }
140   catch (...)
141   {
142     return "";
143   }
144 }
145
146 string CRepository::GetAddonHash(const AddonPtr& addon) const
147 {
148   string checksum;
149   DirList::const_iterator it;
150   for (it = m_dirs.begin();it != m_dirs.end(); ++it)
151     if (URIUtils::IsInPath(addon->Path(), it->datadir))
152       break;
153   if (it != m_dirs.end() && it->hashes)
154   {
155     checksum = FetchChecksum(addon->Path()+".md5");
156     size_t pos = checksum.find_first_of(" \n");
157     if (pos != string::npos)
158       return checksum.substr(0, pos);
159   }
160   return checksum;
161 }
162
163 #define SET_IF_NOT_EMPTY(x,y) \
164   { \
165     if (!x.empty()) \
166        x = y; \
167   }
168
169 VECADDONS CRepository::Parse(const DirInfo& dir)
170 {
171   VECADDONS result;
172   CXBMCTinyXML doc;
173
174   string file = dir.info;
175   if (dir.compressed)
176   {
177     CURL url(dir.info);
178     string opts = url.GetProtocolOptions();
179     if (!opts.empty())
180       opts += "&";
181     url.SetProtocolOptions(opts+"Encoding=gzip");
182     file = url.Get();
183   }
184
185   if (doc.LoadFile(file) && doc.RootElement())
186   {
187     CAddonMgr::Get().AddonsFromRepoXML(doc.RootElement(), result);
188     for (IVECADDONS i = result.begin(); i != result.end(); ++i)
189     {
190       AddonPtr addon = *i;
191       if (dir.zipped)
192       {
193         string file = StringUtils::Format("%s/%s-%s.zip", addon->ID().c_str(), addon->ID().c_str(), addon->Version().c_str());
194         addon->Props().path = URIUtils::AddFileToFolder(dir.datadir,file);
195         SET_IF_NOT_EMPTY(addon->Props().icon,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/icon.png"))
196         file = StringUtils::Format("%s/changelog-%s.txt", addon->ID().c_str(), addon->Version().c_str());
197         SET_IF_NOT_EMPTY(addon->Props().changelog,URIUtils::AddFileToFolder(dir.datadir,file))
198         SET_IF_NOT_EMPTY(addon->Props().fanart,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/fanart.jpg"))
199       }
200       else
201       {
202         addon->Props().path = URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/");
203         SET_IF_NOT_EMPTY(addon->Props().icon,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/icon.png"))
204         SET_IF_NOT_EMPTY(addon->Props().changelog,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/changelog.txt"))
205         SET_IF_NOT_EMPTY(addon->Props().fanart,URIUtils::AddFileToFolder(dir.datadir,addon->ID()+"/fanart.jpg"))
206       }
207     }
208   }
209
210   return result;
211 }
212
213 CRepositoryUpdateJob::CRepositoryUpdateJob(const VECADDONS &repos)
214   : m_repos(repos)
215 {
216 }
217
218 void MergeAddons(map<string, AddonPtr> &addons, const VECADDONS &new_addons)
219 {
220   for (VECADDONS::const_iterator it = new_addons.begin(); it != new_addons.end(); ++it)
221   {
222     map<string, AddonPtr>::iterator existing = addons.find((*it)->ID());
223     if (existing != addons.end())
224     { // already got it - replace if we have a newer version
225       if (existing->second->Version() < (*it)->Version())
226         existing->second = *it;
227     }
228     else
229       addons.insert(make_pair((*it)->ID(), *it));
230   }
231 }
232
233 bool CRepositoryUpdateJob::DoWork()
234 {
235   map<string, AddonPtr> addons;
236   for (VECADDONS::const_iterator i = m_repos.begin(); i != m_repos.end(); ++i)
237   {
238     if (ShouldCancel(0, 0))
239       return false;
240     RepositoryPtr repo = boost::dynamic_pointer_cast<CRepository>(*i);
241     VECADDONS newAddons = GrabAddons(repo);
242     MergeAddons(addons, newAddons);
243   }
244   if (addons.empty())
245     return false;
246
247   // check for updates
248   CAddonDatabase database;
249   database.Open();
250   database.BeginMultipleExecute();
251
252   CTextureDatabase textureDB;
253   textureDB.Open();
254   textureDB.BeginMultipleExecute();
255   VECADDONS notifications;
256   for (map<string, AddonPtr>::const_iterator i = addons.begin(); i != addons.end(); ++i)
257   {
258     // manager told us to feck off
259     if (ShouldCancel(0,0))
260       break;
261
262     AddonPtr newAddon = i->second;
263     bool deps_met = CAddonInstaller::Get().CheckDependencies(newAddon, &database);
264     if (!deps_met && newAddon->Props().broken.empty())
265       newAddon->Props().broken = "DEPSNOTMET";
266
267     // invalidate the art associated with this item
268     if (!newAddon->Props().fanart.empty())
269       textureDB.InvalidateCachedTexture(newAddon->Props().fanart);
270     if (!newAddon->Props().icon.empty())
271       textureDB.InvalidateCachedTexture(newAddon->Props().icon);
272
273     AddonPtr addon;
274     CAddonMgr::Get().GetAddon(newAddon->ID(),addon);
275     if (addon && newAddon->Version() > addon->Version() &&
276         !database.IsAddonBlacklisted(newAddon->ID(),newAddon->Version().c_str()) &&
277         deps_met)
278     {
279       if (CSettings::Get().GetBool("general.addonautoupdate") || addon->Type() >= ADDON_VIZ_LIBRARY)
280       {
281         string referer;
282         if (URIUtils::IsInternetStream(newAddon->Path()))
283           referer = StringUtils::Format("Referer=%s-%s.zip",addon->ID().c_str(),addon->Version().c_str());
284
285         if (newAddon->Type() == ADDON_PVRDLL &&
286             !PVR::CPVRManager::Get().InstallAddonAllowed(newAddon->ID()))
287           PVR::CPVRManager::Get().MarkAsOutdated(addon->ID(), referer);
288         else
289           CAddonInstaller::Get().Install(addon->ID(), true, referer);
290       }
291       else
292         notifications.push_back(addon);
293     }
294
295     // Check if we should mark the add-on as broken.  We may have a newer version
296     // of this add-on in the database or installed - if so, we keep it unbroken.
297     bool haveNewer = (addon && addon->Version() > newAddon->Version()) ||
298                      database.GetAddonVersion(newAddon->ID()) > newAddon->Version();
299     if (!haveNewer)
300     {
301       if (!newAddon->Props().broken.empty())
302       {
303         if (database.IsAddonBroken(newAddon->ID()).empty())
304         {
305           std::string line = g_localizeStrings.Get(24096);
306           if (newAddon->Props().broken == "DEPSNOTMET")
307             line = g_localizeStrings.Get(24104);
308           if (addon && CGUIDialogYesNo::ShowAndGetInput(newAddon->Name(),
309                                                line,
310                                                g_localizeStrings.Get(24097),
311                                                ""))
312             CAddonMgr::Get().DisableAddon(newAddon->ID());
313         }
314       }
315       database.BreakAddon(newAddon->ID(), newAddon->Props().broken);
316     }
317   }
318   database.CommitMultipleExecute();
319   textureDB.CommitMultipleExecute();
320   if (!notifications.empty() && CSettings::Get().GetBool("general.addonnotifications"))
321   {
322     if (notifications.size() == 1)
323       CGUIDialogKaiToast::QueueNotification(notifications[0]->Icon(),
324                                             g_localizeStrings.Get(24061),
325                                             notifications[0]->Name(),TOAST_DISPLAY_TIME,false,TOAST_DISPLAY_TIME);
326     else
327       CGUIDialogKaiToast::QueueNotification("",
328                                             g_localizeStrings.Get(24001),
329                                             g_localizeStrings.Get(24061),TOAST_DISPLAY_TIME,false,TOAST_DISPLAY_TIME);
330   }
331
332   return true;
333 }
334
335 VECADDONS CRepositoryUpdateJob::GrabAddons(RepositoryPtr& repo)
336 {
337   CAddonDatabase database;
338   VECADDONS addons;
339   database.Open();
340   string checksum;
341   database.GetRepoChecksum(repo->ID(),checksum);
342   string reposum;
343
344   /* This for loop is duplicated in CRepository::Checksum().
345    * If you make changes here, they may be applicable there, too.
346    */
347   for (CRepository::DirList::const_iterator it  = repo->m_dirs.begin(); it != repo->m_dirs.end(); ++it)
348   {
349     if (ShouldCancel(0, 0))
350       return addons;
351     if (!it->checksum.empty())
352       reposum += CRepository::FetchChecksum(it->checksum);
353   }
354
355   if (checksum != reposum || checksum.empty())
356   {
357     map<string, AddonPtr> uniqueAddons;
358     for (CRepository::DirList::const_iterator it = repo->m_dirs.begin(); it != repo->m_dirs.end(); ++it)
359     {
360       if (ShouldCancel(0, 0))
361         return addons;
362       VECADDONS addons2 = CRepository::Parse(*it);
363       MergeAddons(uniqueAddons, addons2);
364     }
365
366     if (uniqueAddons.empty())
367     {
368       CLog::Log(LOGERROR,"Repository %s returned no add-ons, listing may have failed",repo->Name().c_str());
369       reposum = checksum; // don't update the checksum
370     }
371     else
372     {
373       bool add=true;
374       if (!repo->Props().libname.empty())
375       {
376         CFileItemList dummy;
377         string s = StringUtils::Format("plugin://%s/?action=update", repo->ID().c_str());
378         add = CDirectory::GetDirectory(s, dummy);
379       }
380       if (add)
381       {
382         for (map<string, AddonPtr>::const_iterator i = uniqueAddons.begin(); i != uniqueAddons.end(); ++i)
383           addons.push_back(i->second);
384         database.AddRepository(repo->ID(),addons,reposum);
385       }
386     }
387   }
388   else
389     database.GetRepository(repo->ID(),addons);
390   database.SetRepoTimestamp(repo->ID(),CDateTime::GetCurrentDateTime().GetAsDBDateTime());
391
392   return addons;
393 }
394