Load/save the Addon enabled/disabled state to the config file
[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<Config::Addon>& addon_config) :
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_addon_config(addon_config),
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
89   // FIXME: We should also restore the order here
90   for(auto& addon : m_addon_config)
91   {
92     if (addon.enabled)
93     {
94       enable_addon(addon.id);
95     }
96   }
97 }
98
99 AddonManager::~AddonManager()
100 {
101   // sync enabled/disabled addons into the config for saving
102   m_addon_config.clear();
103   for(auto& addon : m_installed_addons)
104   {
105     m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
106   }
107 }
108
109 Addon&
110 AddonManager::get_repository_addon(const AddonId& id)
111 {
112   auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
113                          [&id](const std::unique_ptr<Addon>& addon)
114                          {
115                            return addon->get_id() == id;
116                          });
117
118   if (it != m_repository_addons.end())
119   {
120     return **it;
121   }
122   else
123   {
124     throw std::runtime_error("Couldn't find repository Addon with id: " + id);
125   }
126 }
127
128 Addon&
129 AddonManager::get_installed_addon(const AddonId& id)
130 {
131   auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
132                          [&id](const std::unique_ptr<Addon>& addon)
133                          {
134                            return addon->get_id() == id;
135                          });
136
137   if (it != m_installed_addons.end())
138   {
139     return **it;
140   }
141   else
142   {
143     throw std::runtime_error("Couldn't find installed Addon with id: " + id);
144   }
145 }
146
147 std::vector<AddonId>
148 AddonManager::get_repository_addons() const
149 {
150   std::vector<AddonId> results;
151   results.reserve(m_repository_addons.size());
152   std::transform(m_repository_addons.begin(), m_repository_addons.end(),
153                  std::back_inserter(results),
154                  [](const std::unique_ptr<Addon>& addon)
155                  {
156                    return addon->get_id();
157                  });
158   return results;
159 }
160
161
162 std::vector<AddonId>
163 AddonManager::get_installed_addons() const
164 {
165   std::vector<AddonId> results;
166   results.reserve(m_installed_addons.size());
167   std::transform(m_installed_addons.begin(), m_installed_addons.end(),
168                  std::back_inserter(results),
169                  [](const std::unique_ptr<Addon>& addon)
170                  {
171                    return addon->get_id();
172                  });
173   return results;
174 }
175
176 bool
177 AddonManager::has_online_support() const
178 {
179   return true;
180 }
181
182 bool
183 AddonManager::has_been_updated() const
184 {
185   return m_has_been_updated;
186 }
187
188 void
189 AddonManager::check_online()
190 {
191   std::string addoninfos = m_downloader.download(m_repository_url);
192   m_repository_addons = parse_addon_infos(addoninfos);
193   m_has_been_updated = true;
194 }
195
196 void
197 AddonManager::install_addon(const AddonId& addon_id)
198 {
199   { // remove addon if it already exists
200     auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
201                            [&addon_id](const std::unique_ptr<Addon>& addon)
202                            {
203                              return addon->get_id() == addon_id;
204                            });
205     if (it != m_installed_addons.end())
206     {
207       log_debug << "reinstalling addon " << addon_id << std::endl;
208       if ((*it)->is_enabled())
209       {
210         disable_addon((*it)->get_id());
211       }
212       m_installed_addons.erase(it);
213     }
214     else
215     {
216       log_debug << "installing addon " << addon_id << std::endl;
217     }
218   }
219
220   Addon& repository_addon = get_repository_addon(addon_id);
221
222   std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
223
224   m_downloader.download(repository_addon.get_http_url(), install_filename);
225
226   MD5 md5 = md5_from_file(install_filename);
227   if (repository_addon.get_md5() != md5.hex_digest())
228   {
229     if (PHYSFS_delete(install_filename.c_str()) == 0)
230     {
231       log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
232     }
233
234     throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
235   }
236   else
237   {
238     const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
239     if (!realdir)
240     {
241       throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
242     }
243     else
244     {
245       add_installed_archive(install_filename, md5.hex_digest());
246     }
247   }
248 }
249
250 void
251 AddonManager::uninstall_addon(const AddonId& addon_id)
252 {
253   log_debug << "uninstalling addon " << addon_id << std::endl;
254   Addon& addon = get_installed_addon(addon_id);
255   if (addon.is_enabled())
256   {
257     disable_addon(addon_id);
258   }
259   log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
260   PHYSFS_delete(addon.get_install_filename().c_str());
261   m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
262                                           [&addon](const std::unique_ptr<Addon>& rhs)
263                                           {
264                                             return addon.get_id() == rhs->get_id();
265                                           }),
266                            m_installed_addons.end());
267 }
268
269 void
270 AddonManager::enable_addon(const AddonId& addon_id)
271 {
272   log_debug << "enabling addon " << addon_id << std::endl;
273   Addon& addon = get_installed_addon(addon_id);
274   if (addon.is_enabled())
275   {
276     log_warning << "Tried enabling already enabled Add-on" << std::endl;
277   }
278   else
279   {
280     log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
281     //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
282     if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
283     {
284       log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
285                   << PHYSFS_getLastError() << std::endl;
286     }
287     else
288     {
289       addon.set_enabled(true);
290     }
291   }
292 }
293
294 void
295 AddonManager::disable_addon(const AddonId& addon_id)
296 {
297   log_debug << "disabling addon " << addon_id << std::endl;
298   Addon& addon = get_installed_addon(addon_id);
299   if (!addon.is_enabled())
300   {
301     log_warning << "Tried disabling already disabled Add-On" << std::endl;
302   }
303   else
304   {
305     log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
306     if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
307     {
308       log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
309                   << PHYSFS_getLastError() << std::endl;
310     }
311     else
312     {
313       addon.set_enabled(false);
314     }
315   }
316 }
317
318 std::vector<std::string>
319 AddonManager::scan_for_archives() const
320 {
321   std::vector<std::string> archives;
322
323   // Search for archives and add them to the search path
324   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
325     rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
326        PHYSFS_freeList);
327   for(char** i = rc.get(); *i != 0; ++i)
328   {
329     if (has_suffix(*i, ".zip"))
330     {
331       std::string archive = FileSystem::join(m_addon_directory, *i);
332       if (PHYSFS_exists(archive.c_str()))
333       {
334         archives.push_back(archive);
335       }
336     }
337   }
338
339   return archives;
340 }
341
342 std::string
343 AddonManager::scan_for_info(const std::string& archive_os_path) const
344 {
345   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
346     rc2(PHYSFS_enumerateFiles("/"),
347         PHYSFS_freeList);
348   for(char** j = rc2.get(); *j != 0; ++j)
349   {
350     if (has_suffix(*j, ".nfo"))
351     {
352       std::string nfo_filename = FileSystem::join("/", *j);
353
354       // make sure it's in the current archive_os_path
355       const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
356       if (!realdir)
357       {
358         log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
359       }
360       else
361       {
362         if (realdir == archive_os_path)
363         {
364           return nfo_filename;
365         }
366       }
367     }
368   }
369
370   return std::string();
371 }
372
373 void
374 AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
375 {
376   const char* realdir = PHYSFS_getRealDir(archive.c_str());
377   if (!realdir)
378   {
379     log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
380                 << PHYSFS_getLastError() << std::endl;
381   }
382   else
383   {
384     std::string os_path = FileSystem::join(realdir, archive);
385
386     PHYSFS_addToSearchPath(os_path.c_str(), 0);
387
388     std::string nfo_filename = scan_for_info(os_path);
389
390     if (nfo_filename.empty())
391     {
392       log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
393     }
394     else
395     {
396       try
397       {
398         std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
399         addon->set_install_filename(os_path, md5);
400         m_installed_addons.push_back(std::move(addon));
401       }
402       catch (const std::runtime_error& e)
403       {
404         log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
405       }
406     }
407
408     PHYSFS_removeFromSearchPath(os_path.c_str());
409   }
410 }
411
412 void
413 AddonManager::add_installed_addons()
414 {
415   auto archives = scan_for_archives();
416
417   for(auto archive : archives)
418   {
419     MD5 md5 = md5_from_file(archive);
420     add_installed_archive(archive, md5.hex_digest());
421   }
422 }
423
424 AddonManager::AddonList
425 AddonManager::parse_addon_infos(const std::string& addoninfos) const
426 {
427   AddonList m_addons;
428
429   try
430   {
431     lisp::Parser parser;
432     std::stringstream addoninfos_stream(addoninfos);
433     const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
434     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
435     if(!addons_lisp)
436     {
437       throw std::runtime_error("Downloaded file is not an Add-on list");
438     }
439     else
440     {
441       lisp::ListIterator iter(addons_lisp);
442       while(iter.next())
443       {
444         const std::string& token = iter.item();
445         if(token != "supertux-addoninfo")
446         {
447           log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
448         }
449         else
450         {
451           std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
452           m_addons.push_back(std::move(addon));
453         }
454       }
455
456       return m_addons;
457     }
458   }
459   catch(const std::exception& e)
460   {
461     std::stringstream msg;
462     msg << "Problem when reading Add-on list: " << e.what();
463     throw std::runtime_error(msg.str());
464   }
465
466   return m_addons;
467 }
468
469 /* EOF */