Added "Nothing New" indicator after check for new packages with no new results
[supertux.git] / src / addon / addon_manager.cpp
1 //  SuperTux - Add-on Manager
2 //  Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
3 //                2014 Ingo Ruhnke <grumbel@gmail.com>
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 3 of the License, or
8 //  (at your option) 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 this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 #include "addon/addon_manager.hpp"
19
20 #include <config.h>
21 #include <version.h>
22
23 #include <algorithm>
24 #include <iostream>
25 #include <memory>
26 #include <physfs.h>
27 #include <sstream>
28 #include <stdexcept>
29 #include <stdio.h>
30 #include <sys/stat.h>
31
32 #include "addon/addon.hpp"
33 #include "addon/md5.hpp"
34 #include "lisp/list_iterator.hpp"
35 #include "lisp/parser.hpp"
36 #include "util/file_system.hpp"
37 #include "util/log.hpp"
38 #include "util/reader.hpp"
39 #include "util/writer.hpp"
40
41 namespace {
42
43 MD5 md5_from_file(const std::string& filename)
44 {
45   // TODO: this does not work as expected for some files -- IFileStream seems to not always behave like an ifstream.
46   //IFileStream ifs(installed_physfs_filename);
47   //std::string md5 = MD5(ifs).hex_digest();
48
49   MD5 md5;
50
51   unsigned char buffer[1024];
52   PHYSFS_file* file = PHYSFS_openRead(filename.c_str());
53   while (true)
54   {
55     PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
56     if (len <= 0) break;
57     md5.update(buffer, len);
58   }
59   PHYSFS_close(file);
60
61   return md5;
62 }
63
64 bool has_suffix(const std::string& str, const std::string& suffix)
65 {
66   if (str.length() >= suffix.length())
67     return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
68   else
69     return false;
70 }
71
72 } // namespace
73
74 AddonManager::AddonManager(const std::string& addon_directory,
75                            std::vector<std::string>& ignored_addon_ids) :
76   m_downloader(),
77   m_addon_directory(addon_directory),
78   //m_repository_url("http://addons.supertux.googlecode.com/git/index-0_3_5.nfo"),
79   m_repository_url("http://localhost:8000/index-0_4_0.nfo"),
80   m_ignored_addon_ids(ignored_addon_ids),
81   m_installed_addons(),
82   m_repository_addons(),
83   m_has_been_updated(false)
84 {
85   PHYSFS_mkdir(m_addon_directory.c_str());
86
87   add_installed_addons();
88   for(auto& addon : m_installed_addons)
89   {
90     if (std::find(m_ignored_addon_ids.begin(), m_ignored_addon_ids.end(),
91                   addon->get_id()) != m_ignored_addon_ids.end())
92     {
93       enable_addon(addon->get_id());
94     }
95   }
96 }
97
98 AddonManager::~AddonManager()
99 {
100 }
101
102 Addon&
103 AddonManager::get_repository_addon(const AddonId& id)
104 {
105   auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
106                          [&id](const std::unique_ptr<Addon>& addon)
107                          {
108                            return addon->get_id() == id;
109                          });
110
111   if (it != m_repository_addons.end())
112   {
113     return **it;
114   }
115   else
116   {
117     throw std::runtime_error("Couldn't find repository Addon with id: " + id);
118   }
119 }
120
121 Addon&
122 AddonManager::get_installed_addon(const AddonId& id)
123 {
124   auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
125                          [&id](const std::unique_ptr<Addon>& addon)
126                          {
127                            return addon->get_id() == id;
128                          });
129
130   if (it != m_installed_addons.end())
131   {
132     return **it;
133   }
134   else
135   {
136     throw std::runtime_error("Couldn't find installed Addon with id: " + id);
137   }
138 }
139
140 std::vector<AddonId>
141 AddonManager::get_repository_addons() const
142 {
143   std::vector<AddonId> results;
144   results.reserve(m_repository_addons.size());
145   std::transform(m_repository_addons.begin(), m_repository_addons.end(),
146                  std::back_inserter(results),
147                  [](const std::unique_ptr<Addon>& addon)
148                  {
149                    return addon->get_id();
150                  });
151   return results;
152 }
153
154
155 std::vector<AddonId>
156 AddonManager::get_installed_addons() const
157 {
158   std::vector<AddonId> results;
159   results.reserve(m_installed_addons.size());
160   std::transform(m_installed_addons.begin(), m_installed_addons.end(),
161                  std::back_inserter(results),
162                  [](const std::unique_ptr<Addon>& addon)
163                  {
164                    return addon->get_id();
165                  });
166   return results;
167 }
168
169 bool
170 AddonManager::has_online_support() const
171 {
172   return true;
173 }
174
175 bool
176 AddonManager::has_been_updated() const
177 {
178   return m_has_been_updated;
179 }
180
181 void
182 AddonManager::check_online()
183 {
184   std::string addoninfos = m_downloader.download(m_repository_url);
185   m_repository_addons = parse_addon_infos(addoninfos);
186   m_has_been_updated = true;
187 }
188
189 void
190 AddonManager::install_addon(const AddonId& addon_id)
191 {
192   { // remove addon if it already exists
193     auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
194                            [&addon_id](const std::unique_ptr<Addon>& addon)
195                            {
196                              return addon->get_id() == addon_id;
197                            });
198     if (it != m_installed_addons.end())
199     {
200       log_debug << "reinstalling addon " << addon_id << std::endl;
201       if ((*it)->is_enabled())
202       {
203         disable_addon((*it)->get_id());
204       }
205       m_installed_addons.erase(it);
206     }
207     else
208     {
209       log_debug << "installing addon " << addon_id << std::endl;
210     }
211   }
212
213   Addon& repository_addon = get_repository_addon(addon_id);
214
215   std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
216
217   m_downloader.download(repository_addon.get_http_url(), install_filename);
218
219   MD5 md5 = md5_from_file(install_filename);
220   if (repository_addon.get_md5() != md5.hex_digest())
221   {
222     if (PHYSFS_delete(install_filename.c_str()) == 0)
223     {
224       log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
225     }
226
227     throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
228   }
229   else
230   {
231     const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
232     if (!realdir)
233     {
234       throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
235     }
236     else
237     {
238       add_installed_archive(install_filename, md5.hex_digest());
239     }
240   }
241 }
242
243 void
244 AddonManager::uninstall_addon(const AddonId& addon_id)
245 {
246   log_debug << "uninstalling addon " << addon_id << std::endl;
247   Addon& addon = get_installed_addon(addon_id);
248   if (addon.is_enabled())
249   {
250     disable_addon(addon_id);
251   }
252   log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
253   PHYSFS_delete(addon.get_install_filename().c_str());
254   m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
255                                           [&addon](const std::unique_ptr<Addon>& rhs)
256                                           {
257                                             return addon.get_id() == rhs->get_id();
258                                           }),
259                            m_installed_addons.end());
260 }
261
262 void
263 AddonManager::enable_addon(const AddonId& addon_id)
264 {
265   log_debug << "enabling addon " << addon_id << std::endl;
266   Addon& addon = get_installed_addon(addon_id);
267   if (addon.is_enabled())
268   {
269     log_warning << "Tried enabling already enabled Add-on" << std::endl;
270   }
271   else
272   {
273     log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
274     //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
275     if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
276     {
277       log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
278                   << PHYSFS_getLastError() << std::endl;
279     }
280     else
281     {
282       addon.set_enabled(true);
283     }
284   }
285 }
286
287 void
288 AddonManager::disable_addon(const AddonId& addon_id)
289 {
290   log_debug << "disabling addon " << addon_id << std::endl;
291   Addon& addon = get_installed_addon(addon_id);
292   if (!addon.is_enabled())
293   {
294     log_warning << "Tried disabling already disabled Add-On" << std::endl;
295   }
296   else
297   {
298     log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
299     if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
300     {
301       log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
302                   << PHYSFS_getLastError() << std::endl;
303     }
304     else
305     {
306       addon.set_enabled(false);
307     }
308   }
309 }
310
311 std::vector<std::string>
312 AddonManager::scan_for_archives() const
313 {
314   std::vector<std::string> archives;
315
316   // Search for archives and add them to the search path
317   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
318     rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
319        PHYSFS_freeList);
320   for(char** i = rc.get(); *i != 0; ++i)
321   {
322     if (has_suffix(*i, ".zip"))
323     {
324       std::string archive = FileSystem::join(m_addon_directory, *i);
325       if (PHYSFS_exists(archive.c_str()))
326       {
327         archives.push_back(archive);
328       }
329     }
330   }
331
332   return archives;
333 }
334
335 std::string
336 AddonManager::scan_for_info(const std::string& archive_os_path) const
337 {
338   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
339     rc2(PHYSFS_enumerateFiles("/"),
340         PHYSFS_freeList);
341   for(char** j = rc2.get(); *j != 0; ++j)
342   {
343     log_debug << "enumerating: " << std::string(*j) << std::endl;
344     if (has_suffix(*j, ".nfo"))
345     {
346       std::string nfo_filename = FileSystem::join("/", *j);
347
348       // make sure it's in the current archive_os_path
349       const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
350       if (!realdir)
351       {
352         log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
353       }
354       else
355       {
356         log_debug << "compare: " << realdir << " " << archive_os_path << std::endl;
357         if (realdir == archive_os_path)
358         {
359           return nfo_filename;
360         }
361       }
362     }
363   }
364
365   return std::string();
366 }
367
368 void
369 AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
370 {
371   const char* realdir = PHYSFS_getRealDir(archive.c_str());
372   if (!realdir)
373   {
374     log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
375                 << PHYSFS_getLastError() << std::endl;
376   }
377   else
378   {
379     std::string os_path = FileSystem::join(realdir, archive);
380
381     PHYSFS_addToSearchPath(os_path.c_str(), 0);
382
383     std::string nfo_filename = scan_for_info(os_path);
384
385     if (nfo_filename.empty())
386     {
387       log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
388     }
389     else
390     {
391       try
392       {
393         std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
394         addon->set_install_filename(os_path, md5);
395         m_installed_addons.push_back(std::move(addon));
396       }
397       catch (const std::runtime_error& e)
398       {
399         log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
400       }
401     }
402
403     PHYSFS_removeFromSearchPath(os_path.c_str());
404   }
405 }
406
407 void
408 AddonManager::add_installed_addons()
409 {
410   auto archives = scan_for_archives();
411
412   for(auto archive : archives)
413   {
414     MD5 md5 = md5_from_file(archive);
415     add_installed_archive(archive, md5.hex_digest());
416   }
417 }
418
419 AddonManager::AddonList
420 AddonManager::parse_addon_infos(const std::string& addoninfos) const
421 {
422   AddonList m_addons;
423
424   try
425   {
426     lisp::Parser parser;
427     std::stringstream addoninfos_stream(addoninfos);
428     const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
429     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
430     if(!addons_lisp)
431     {
432       throw std::runtime_error("Downloaded file is not an Add-on list");
433     }
434     else
435     {
436       lisp::ListIterator iter(addons_lisp);
437       while(iter.next())
438       {
439         const std::string& token = iter.item();
440         if(token != "supertux-addoninfo")
441         {
442           log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
443         }
444         else
445         {
446           std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
447           m_addons.push_back(std::move(addon));
448         }
449       }
450
451       return m_addons;
452     }
453   }
454   catch(const std::exception& e)
455   {
456     std::stringstream msg;
457     msg << "Problem when reading Add-on list: " << e.what();
458     throw std::runtime_error(msg.str());
459   }
460
461   return m_addons;
462 }
463
464 /* EOF */