934dc1d768fa65e78ddb5f03ac2947bcd44b281c
[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   if (!file)
54   {
55     std::ostringstream out;
56     out << "PHYSFS_openRead() failed: " << PHYSFS_getLastError();
57     throw std::runtime_error(out.str());
58   }
59   else
60   {
61     while (true)
62     {
63       PHYSFS_sint64 len = PHYSFS_read(file, buffer, 1, sizeof(buffer));
64       if (len <= 0) break;
65       md5.update(buffer, len);
66     }
67     PHYSFS_close(file);
68
69     return md5;
70   }
71 }
72
73 bool has_suffix(const std::string& str, const std::string& suffix)
74 {
75   if (str.length() >= suffix.length())
76     return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
77   else
78     return false;
79 }
80
81 } // namespace
82
83 AddonManager::AddonManager(const std::string& addon_directory,
84                            std::vector<Config::Addon>& addon_config) :
85   m_downloader(),
86   m_addon_directory(addon_directory),
87   m_repository_url("http://addons.supertux.googlecode.com/git/index-0_4_0.nfo"),
88   m_addon_config(addon_config),
89   m_installed_addons(),
90   m_repository_addons(),
91   m_has_been_updated(false),
92   m_transfer_status()
93 {
94   PHYSFS_mkdir(m_addon_directory.c_str());
95
96   add_installed_addons();
97
98   // FIXME: We should also restore the order here
99   for(auto& addon : m_addon_config)
100   {
101     if (addon.enabled)
102     {
103       try
104       {
105         enable_addon(addon.id);
106       }
107       catch(const std::exception& err)
108       {
109         log_warning << "failed to enable addon from config: " << err.what() << std::endl;
110       }
111     }
112   }
113 }
114
115 AddonManager::~AddonManager()
116 {
117   // sync enabled/disabled addons into the config for saving
118   m_addon_config.clear();
119   for(auto& addon : m_installed_addons)
120   {
121     m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
122   }
123 }
124
125 Addon&
126 AddonManager::get_repository_addon(const AddonId& id)
127 {
128   auto it = std::find_if(m_repository_addons.begin(), m_repository_addons.end(),
129                          [&id](const std::unique_ptr<Addon>& addon)
130                          {
131                            return addon->get_id() == id;
132                          });
133
134   if (it != m_repository_addons.end())
135   {
136     return **it;
137   }
138   else
139   {
140     throw std::runtime_error("Couldn't find repository Addon with id: " + id);
141   }
142 }
143
144 Addon&
145 AddonManager::get_installed_addon(const AddonId& id)
146 {
147   auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
148                          [&id](const std::unique_ptr<Addon>& addon)
149                          {
150                            return addon->get_id() == id;
151                          });
152
153   if (it != m_installed_addons.end())
154   {
155     return **it;
156   }
157   else
158   {
159     throw std::runtime_error("Couldn't find installed Addon with id: " + id);
160   }
161 }
162
163 std::vector<AddonId>
164 AddonManager::get_repository_addons() const
165 {
166   std::vector<AddonId> results;
167   results.reserve(m_repository_addons.size());
168   std::transform(m_repository_addons.begin(), m_repository_addons.end(),
169                  std::back_inserter(results),
170                  [](const std::unique_ptr<Addon>& addon)
171                  {
172                    return addon->get_id();
173                  });
174   return results;
175 }
176
177
178 std::vector<AddonId>
179 AddonManager::get_installed_addons() const
180 {
181   std::vector<AddonId> results;
182   results.reserve(m_installed_addons.size());
183   std::transform(m_installed_addons.begin(), m_installed_addons.end(),
184                  std::back_inserter(results),
185                  [](const std::unique_ptr<Addon>& addon)
186                  {
187                    return addon->get_id();
188                  });
189   return results;
190 }
191
192 bool
193 AddonManager::has_online_support() const
194 {
195   return true;
196 }
197
198 bool
199 AddonManager::has_been_updated() const
200 {
201   return m_has_been_updated;
202 }
203
204 TransferStatusPtr
205 AddonManager::request_check_online()
206 {
207   if (m_transfer_status)
208   {
209     throw std::runtime_error("only async request can be made to AddonManager at a time");
210   }
211   else
212   {
213     m_transfer_status = m_downloader.request_download(m_repository_url, "/addons/repository.nfo");
214
215     m_transfer_status->then([this]{
216         m_repository_addons = parse_addon_infos("/addons/repository.nfo");
217         m_has_been_updated = true;
218
219         m_transfer_status = {};
220       });
221
222     return m_transfer_status;
223   }
224 }
225
226 void
227 AddonManager::check_online()
228 {
229   m_downloader.download(m_repository_url, "/addons/repository.nfo");
230   m_repository_addons = parse_addon_infos("/addons/repository.nfo");
231   m_has_been_updated = true;
232 }
233
234 TransferStatusPtr
235 AddonManager::request_install_addon(const AddonId& addon_id)
236 {
237   if (m_transfer_status)
238   {
239     throw std::runtime_error("only one addon install request allowed at a time");
240   }
241   else
242   {
243     { // remove addon if it already exists
244       auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
245                              [&addon_id](const std::unique_ptr<Addon>& addon)
246                              {
247                                return addon->get_id() == addon_id;
248                              });
249       if (it != m_installed_addons.end())
250       {
251         log_debug << "reinstalling addon " << addon_id << std::endl;
252         if ((*it)->is_enabled())
253         {
254           disable_addon((*it)->get_id());
255         }
256         m_installed_addons.erase(it);
257       }
258       else
259       {
260         log_debug << "installing addon " << addon_id << std::endl;
261       }
262     }
263
264     Addon& addon = get_repository_addon(addon_id);
265
266     std::string install_filename = FileSystem::join(m_addon_directory, addon.get_filename());
267
268     m_transfer_status = m_downloader.request_download(addon.get_url(), install_filename);
269
270     m_transfer_status->then(
271       [this, install_filename, addon_id]
272       {
273         // complete the addon install
274         Addon& repository_addon = get_repository_addon(addon_id);
275
276         MD5 md5 = md5_from_file(install_filename);
277         if (repository_addon.get_md5() != md5.hex_digest())
278         {
279           if (PHYSFS_delete(install_filename.c_str()) == 0)
280           {
281             log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
282           }
283
284           throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
285         }
286         else
287         {
288           const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
289           if (!realdir)
290           {
291             throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
292           }
293           else
294           {
295             add_installed_archive(install_filename, md5.hex_digest());
296           }
297         }
298
299         m_transfer_status = {};
300       });
301
302     return m_transfer_status;
303   }
304 }
305
306 void
307 AddonManager::install_addon(const AddonId& addon_id)
308 {
309   { // remove addon if it already exists
310     auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
311                            [&addon_id](const std::unique_ptr<Addon>& addon)
312                            {
313                              return addon->get_id() == addon_id;
314                            });
315     if (it != m_installed_addons.end())
316     {
317       log_debug << "reinstalling addon " << addon_id << std::endl;
318       if ((*it)->is_enabled())
319       {
320         disable_addon((*it)->get_id());
321       }
322       m_installed_addons.erase(it);
323     }
324     else
325     {
326       log_debug << "installing addon " << addon_id << std::endl;
327     }
328   }
329
330   Addon& repository_addon = get_repository_addon(addon_id);
331
332   std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
333
334   m_downloader.download(repository_addon.get_url(), install_filename);
335
336   MD5 md5 = md5_from_file(install_filename);
337   if (repository_addon.get_md5() != md5.hex_digest())
338   {
339     if (PHYSFS_delete(install_filename.c_str()) == 0)
340     {
341       log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastError() << std::endl;
342     }
343
344     throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
345   }
346   else
347   {
348     const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
349     if (!realdir)
350     {
351       throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
352     }
353     else
354     {
355       add_installed_archive(install_filename, md5.hex_digest());
356     }
357   }
358 }
359
360 void
361 AddonManager::uninstall_addon(const AddonId& addon_id)
362 {
363   log_debug << "uninstalling addon " << addon_id << std::endl;
364   Addon& addon = get_installed_addon(addon_id);
365   if (addon.is_enabled())
366   {
367     disable_addon(addon_id);
368   }
369   log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
370   PHYSFS_delete(addon.get_install_filename().c_str());
371   m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
372                                           [&addon](const std::unique_ptr<Addon>& rhs)
373                                           {
374                                             return addon.get_id() == rhs->get_id();
375                                           }),
376                            m_installed_addons.end());
377 }
378
379 void
380 AddonManager::enable_addon(const AddonId& addon_id)
381 {
382   log_debug << "enabling addon " << addon_id << std::endl;
383   Addon& addon = get_installed_addon(addon_id);
384   if (addon.is_enabled())
385   {
386     log_warning << "Tried enabling already enabled Add-on" << std::endl;
387   }
388   else
389   {
390     log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
391     //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
392     if (PHYSFS_addToSearchPath(addon.get_install_filename().c_str(), 0) == 0)
393     {
394       log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
395                   << PHYSFS_getLastError() << std::endl;
396     }
397     else
398     {
399       addon.set_enabled(true);
400     }
401   }
402 }
403
404 void
405 AddonManager::disable_addon(const AddonId& addon_id)
406 {
407   log_debug << "disabling addon " << addon_id << std::endl;
408   Addon& addon = get_installed_addon(addon_id);
409   if (!addon.is_enabled())
410   {
411     log_warning << "Tried disabling already disabled Add-On" << std::endl;
412   }
413   else
414   {
415     log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
416     if (PHYSFS_removeFromSearchPath(addon.get_install_filename().c_str()) == 0)
417     {
418       log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
419                   << PHYSFS_getLastError() << std::endl;
420     }
421     else
422     {
423       addon.set_enabled(false);
424     }
425   }
426 }
427
428 std::vector<std::string>
429 AddonManager::scan_for_archives() const
430 {
431   std::vector<std::string> archives;
432
433   // Search for archives and add them to the search path
434   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
435     rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
436        PHYSFS_freeList);
437   for(char** i = rc.get(); *i != 0; ++i)
438   {
439     if (has_suffix(*i, ".zip"))
440     {
441       std::string archive = FileSystem::join(m_addon_directory, *i);
442       if (PHYSFS_exists(archive.c_str()))
443       {
444         archives.push_back(archive);
445       }
446     }
447   }
448
449   return archives;
450 }
451
452 std::string
453 AddonManager::scan_for_info(const std::string& archive_os_path) const
454 {
455   std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
456     rc2(PHYSFS_enumerateFiles("/"),
457         PHYSFS_freeList);
458   for(char** j = rc2.get(); *j != 0; ++j)
459   {
460     if (has_suffix(*j, ".nfo"))
461     {
462       std::string nfo_filename = FileSystem::join("/", *j);
463
464       // make sure it's in the current archive_os_path
465       const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
466       if (!realdir)
467       {
468         log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastError() << std::endl;
469       }
470       else
471       {
472         if (realdir == archive_os_path)
473         {
474           return nfo_filename;
475         }
476       }
477     }
478   }
479
480   return std::string();
481 }
482
483 void
484 AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
485 {
486   const char* realdir = PHYSFS_getRealDir(archive.c_str());
487   if (!realdir)
488   {
489     log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
490                 << PHYSFS_getLastError() << std::endl;
491   }
492   else
493   {
494     std::string os_path = FileSystem::join(realdir, archive);
495
496     PHYSFS_addToSearchPath(os_path.c_str(), 0);
497
498     std::string nfo_filename = scan_for_info(os_path);
499
500     if (nfo_filename.empty())
501     {
502       log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
503     }
504     else
505     {
506       try
507       {
508         std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
509         addon->set_install_filename(os_path, md5);
510         m_installed_addons.push_back(std::move(addon));
511       }
512       catch (const std::runtime_error& e)
513       {
514         log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
515       }
516     }
517
518     PHYSFS_removeFromSearchPath(os_path.c_str());
519   }
520 }
521
522 void
523 AddonManager::add_installed_addons()
524 {
525   auto archives = scan_for_archives();
526
527   for(auto archive : archives)
528   {
529     MD5 md5 = md5_from_file(archive);
530     add_installed_archive(archive, md5.hex_digest());
531   }
532 }
533
534 AddonManager::AddonList
535 AddonManager::parse_addon_infos(const std::string& filename) const
536 {
537   AddonList m_addons;
538
539   try
540   {
541     lisp::Parser parser;
542     const lisp::Lisp* root = parser.parse(filename);
543     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
544     if(!addons_lisp)
545     {
546       throw std::runtime_error("Downloaded file is not an Add-on list");
547     }
548     else
549     {
550       lisp::ListIterator iter(addons_lisp);
551       while(iter.next())
552       {
553         const std::string& token = iter.item();
554         if(token != "supertux-addoninfo")
555         {
556           log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
557         }
558         else
559         {
560           std::unique_ptr<Addon> addon = Addon::parse(*iter.lisp());
561           m_addons.push_back(std::move(addon));
562         }
563       }
564
565       return m_addons;
566     }
567   }
568   catch(const std::exception& e)
569   {
570     std::stringstream msg;
571     msg << "Problem when reading Add-on list: " << e.what();
572     throw std::runtime_error(msg.str());
573   }
574
575   return m_addons;
576 }
577
578 void
579 AddonManager::update()
580 {
581   m_downloader.update();
582 }
583
584 void
585 AddonManager::abort_install()
586 {
587   log_info << "addon install aborted" << std::endl;
588
589   m_downloader.abort(m_transfer_status->id);
590
591   m_transfer_status = {};
592 }
593
594 /* EOF */