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