First round of cleanup up the AddonManager a bit
[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 //
4 //  This program is free software: you can redistribute it and/or modify
5 //  it under the terms of the GNU General Public License as published by
6 //  the Free Software Foundation, either version 3 of the License, or
7 //  (at your option) any later version.
8 //
9 //  This program is distributed in the hope that it will be useful,
10 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 //  GNU General Public License for more details.
13 //
14 //  You should have received a copy of the GNU General Public License
15 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 #include "addon/addon_manager.hpp"
18
19 #include <config.h>
20 #include <version.h>
21
22 #include <algorithm>
23 #include <memory>
24 #include <physfs.h>
25 #include <sstream>
26 #include <stdexcept>
27 #include <sys/stat.h>
28
29 #ifdef HAVE_LIBCURL
30 #  include <curl/curl.h>
31 #  include <curl/easy.h>
32 #endif
33
34 #include "addon/addon.hpp"
35 #include "lisp/list_iterator.hpp"
36 #include "lisp/parser.hpp"
37 #include "util/reader.hpp"
38 #include "util/writer.hpp"
39 #include "util/log.hpp"
40
41 #ifdef HAVE_LIBCURL
42 namespace {
43
44 size_t my_curl_string_append(void *ptr, size_t size, size_t nmemb, void *string_ptr)
45 {
46   std::string& s = *static_cast<std::string*>(string_ptr);
47   std::string buf(static_cast<char*>(ptr), size * nmemb);
48   s += buf;
49   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
50   return size * nmemb;
51 }
52
53 size_t my_curl_physfs_write(void *ptr, size_t size, size_t nmemb, void *f_p)
54 {
55   PHYSFS_file* f = static_cast<PHYSFS_file*>(f_p);
56   PHYSFS_sint64 written = PHYSFS_write(f, ptr, size, nmemb);
57   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
58   return size * written;
59 }
60
61 }
62 #endif
63
64 AddonManager::AddonManager(std::vector<std::string>& ignored_addon_filenames) :
65   m_addons(),
66   m_ignored_addon_filenames(ignored_addon_filenames)
67 {
68 #ifdef HAVE_LIBCURL
69   curl_global_init(CURL_GLOBAL_ALL);
70 #endif
71 }
72
73 AddonManager::~AddonManager()
74 {
75 #ifdef HAVE_LIBCURL
76   curl_global_cleanup();
77 #endif
78 }
79
80 Addon&
81 AddonManager::get_addon(int id)
82 {
83   if (0 <= id && id < static_cast<int>(m_addons.size()))
84   {
85     return *m_addons[id];
86   }
87   else
88   {
89     throw std::runtime_error("AddonManager::get_addon(): id out of range: " + std::to_string(id));
90   }
91 }
92
93 const std::vector<std::unique_ptr<Addon> >&
94 AddonManager::get_addons() const
95 {
96   /*
97     for (std::vector<Addon>::iterator it = installed_addons.begin(); it != installed_addons.end(); ++it) {
98     Addon& addon = *it;
99     if (addon.md5 == "") addon.md5 = calculate_md5(addon);
100     }
101   */
102   return m_addons;
103 }
104
105 bool
106 AddonManager::has_online_support() const
107 {
108 #ifdef HAVE_LIBCURL
109   return true;
110 #else
111   return false;
112 #endif
113 }
114
115 void
116 AddonManager::check_online()
117 {
118 #ifdef HAVE_LIBCURL
119   char error_buffer[CURL_ERROR_SIZE+1];
120
121   const char* baseUrl = "http://addons.supertux.googlecode.com/git/index-0_3_5.nfo";
122   std::string addoninfos = "";
123
124   CURL *curl_handle;
125   curl_handle = curl_easy_init();
126   curl_easy_setopt(curl_handle, CURLOPT_URL, baseUrl);
127   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
128   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_string_append);
129   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &addoninfos);
130   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
131   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
132   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
133   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
134   curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
135   CURLcode result = curl_easy_perform(curl_handle);
136   curl_easy_cleanup(curl_handle);
137
138   if (result != CURLE_OK) {
139     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
140     throw std::runtime_error("Downloading Add-on list failed: " + why);
141   }
142
143   try {
144     lisp::Parser parser;
145     std::stringstream addoninfos_stream(addoninfos);
146     const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
147
148     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
149     if(!addons_lisp) throw std::runtime_error("Downloaded file is not an Add-on list");
150
151     lisp::ListIterator iter(addons_lisp);
152     while(iter.next())
153     {
154       const std::string& token = iter.item();
155       if(token != "supertux-addoninfo")
156       {
157         log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
158         continue;
159       }
160       std::unique_ptr<Addon> addon(new Addon(m_addons.size()));
161       addon->parse(*(iter.lisp()));
162       addon->installed = false;
163       addon->loaded = false;
164
165       // make sure the list of known Add-ons does not already contain this one
166       bool exists = false;
167       for (auto i = m_addons.begin(); i != m_addons.end(); ++i) {
168         if (**i == *addon) {
169           exists = true;
170           break;
171         }
172       }
173
174       if (exists)
175       {
176         // do nothing
177       }
178       else if (addon->suggested_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos)
179       {
180         // make sure the Add-on's file name does not contain weird characters
181         log_warning << "Add-on \"" << addon->title << "\" contains unsafe file name. Skipping." << std::endl;
182       }
183       else
184       {
185         m_addons.push_back(std::move(addon));
186       }
187     }
188   } catch(std::exception& e) {
189     std::stringstream msg;
190     msg << "Problem when reading Add-on list: " << e.what();
191     throw std::runtime_error(msg.str());
192   }
193
194 #endif
195 }
196
197 void
198 AddonManager::install(Addon& addon)
199 {
200 #ifdef HAVE_LIBCURL
201
202   if (addon.installed) throw std::runtime_error("Tried installing installed Add-on");
203
204   // make sure the Add-on's file name does not contain weird characters
205   if (addon.suggested_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
206     throw std::runtime_error("Add-on has unsafe file name (\""+addon.suggested_filename+"\")");
207   }
208
209   std::string fileName = addon.suggested_filename;
210
211   // make sure its file doesn't already exist
212   if (PHYSFS_exists(fileName.c_str())) {
213     fileName = addon.stored_md5 + "_" + addon.suggested_filename;
214     if (PHYSFS_exists(fileName.c_str())) {
215       throw std::runtime_error("Add-on of suggested filename already exists (\""+addon.suggested_filename+"\", \""+fileName+"\")");
216     }
217   }
218
219   char error_buffer[CURL_ERROR_SIZE+1];
220
221   char* url = (char*)malloc(addon.http_url.length() + 1);
222   strncpy(url, addon.http_url.c_str(), addon.http_url.length() + 1);
223
224   PHYSFS_file* f = PHYSFS_openWrite(fileName.c_str());
225
226   log_debug << "Downloading \"" << url << "\"" << std::endl;
227
228   CURL *curl_handle;
229   curl_handle = curl_easy_init();
230   curl_easy_setopt(curl_handle, CURLOPT_URL, url);
231   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
232   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_physfs_write);
233   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, f);
234   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
235   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
236   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
237   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
238   CURLcode result = curl_easy_perform(curl_handle);
239   curl_easy_cleanup(curl_handle);
240
241   PHYSFS_close(f);
242
243   free(url);
244
245   if (result != CURLE_OK) {
246     PHYSFS_delete(fileName.c_str());
247     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
248     throw std::runtime_error("Downloading Add-on failed: " + why);
249   }
250
251   addon.installed = true;
252   addon.installed_physfs_filename = fileName;
253   static const std::string writeDir = PHYSFS_getWriteDir();
254   static const std::string dirSep = PHYSFS_getDirSeparator();
255   addon.installed_absolute_filename = writeDir + dirSep + fileName;
256   addon.loaded = false;
257
258   if (addon.get_md5() != addon.stored_md5) {
259     addon.installed = false;
260     PHYSFS_delete(fileName.c_str());
261     std::string why = "MD5 checksums differ";
262     throw std::runtime_error("Downloading Add-on failed: " + why);
263   }
264
265   log_debug << "Finished downloading \"" << addon.installed_absolute_filename << "\". Enabling Add-on." << std::endl;
266
267   enable(addon);
268
269 #else
270   (void) addon;
271 #endif
272
273 }
274
275 void
276 AddonManager::remove(Addon& addon)
277 {
278   if (!addon.installed) throw std::runtime_error("Tried removing non-installed Add-on");
279
280   //FIXME: more checks
281
282   // make sure the Add-on's file name does not contain weird characters
283   if (addon.installed_physfs_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
284     throw std::runtime_error("Add-on has unsafe file name (\""+addon.installed_physfs_filename+"\")");
285   }
286
287   unload(addon);
288
289   log_debug << "deleting file \"" << addon.installed_absolute_filename << "\"" << std::endl;
290   PHYSFS_delete(addon.installed_absolute_filename.c_str());
291   addon.installed = false;
292
293   // FIXME: As we don't know anything more about it (e.g. where to get it), remove it from list of known Add-ons
294 }
295
296 void
297 AddonManager::disable(Addon& addon)
298 {
299   unload(addon);
300
301   std::string fileName = addon.installed_physfs_filename;
302   if (std::find(m_ignored_addon_filenames.begin(), m_ignored_addon_filenames.end(), fileName) == m_ignored_addon_filenames.end()) {
303     m_ignored_addon_filenames.push_back(fileName);
304   }
305 }
306
307 void
308 AddonManager::enable(Addon& addon)
309 {
310   load(addon);
311
312   std::string fileName = addon.installed_physfs_filename;
313   std::vector<std::string>::iterator i = std::find(m_ignored_addon_filenames.begin(), m_ignored_addon_filenames.end(), fileName);
314   if (i != m_ignored_addon_filenames.end()) {
315     m_ignored_addon_filenames.erase(i);
316   }
317 }
318
319 void
320 AddonManager::unload(Addon& addon)
321 {
322   if (!addon.installed) throw std::runtime_error("Tried unloading non-installed Add-on");
323   if (!addon.loaded) return;
324
325   log_debug << "Removing archive \"" << addon.installed_absolute_filename << "\" from search path" << std::endl;
326   if (PHYSFS_removeFromSearchPath(addon.installed_absolute_filename.c_str()) == 0) {
327     log_warning << "Could not remove " << addon.installed_absolute_filename << " from search path. Ignoring." << std::endl;
328     return;
329   }
330
331   addon.loaded = false;
332 }
333
334 void
335 AddonManager::load(Addon& addon)
336 {
337   if (!addon.installed) throw std::runtime_error("Tried loading non-installed Add-on");
338   if (addon.loaded) return;
339
340   log_debug << "Adding archive \"" << addon.installed_absolute_filename << "\" to search path" << std::endl;
341   if (PHYSFS_addToSearchPath(addon.installed_absolute_filename.c_str(), 0) == 0) {
342     log_warning << "Could not add " << addon.installed_absolute_filename << " to search path. Ignoring." << std::endl;
343     return;
344   }
345
346   addon.loaded = true;
347 }
348
349 void
350 AddonManager::load_addons()
351 {
352   // unload all Addons and forget about them
353   for (auto i = m_addons.begin(); i != m_addons.end(); ++i)
354   {
355     if ((*i)->installed && (*i)->loaded)
356     {
357       unload(**i);
358     }
359   }
360   m_addons.clear();
361
362   // Search for archives and add them to the search path
363   char** rc = PHYSFS_enumerateFiles("/");
364
365   for(char** i = rc; *i != 0; ++i) {
366
367     // get filename of potential archive
368     std::string fileName = *i;
369
370     const std::string archiveDir = PHYSFS_getRealDir(fileName.c_str());
371     static const std::string dirSep = PHYSFS_getDirSeparator();
372     std::string fullFilename = archiveDir + dirSep + fileName;
373
374     /*
375     // make sure it's in the writeDir
376     static const std::string writeDir = PHYSFS_getWriteDir();
377     if (fileName.compare(0, writeDir.length(), writeDir) != 0) continue;
378     */
379
380     // make sure it looks like an archive
381     static const std::string archiveExt = ".zip";
382     if (fullFilename.compare(fullFilename.length()-archiveExt.length(), archiveExt.length(), archiveExt) != 0) continue;
383
384     // make sure it exists
385     struct stat stats;
386     if (stat(fullFilename.c_str(), &stats) != 0) continue;
387
388     // make sure it's an actual file
389     if (!S_ISREG(stats.st_mode)) continue;
390
391     log_debug << "Found archive \"" << fullFilename << "\"" << std::endl;
392
393     // add archive to search path
394     PHYSFS_addToSearchPath(fullFilename.c_str(), 0);
395
396     // Search for infoFiles
397     std::string infoFileName = "";
398     char** rc2 = PHYSFS_enumerateFiles("/");
399     for(char** j = rc2; *j != 0; ++j) {
400
401       // get filename of potential infoFile
402       std::string potentialInfoFileName = *j;
403
404       // make sure it looks like an infoFile
405       static const std::string infoExt = ".nfo";
406       if (potentialInfoFileName.length() <= infoExt.length())
407         continue;
408
409       if (potentialInfoFileName.compare(potentialInfoFileName.length()-infoExt.length(), infoExt.length(), infoExt) != 0)
410         continue;
411
412       // make sure it's in the current archive
413       std::string infoFileDir = PHYSFS_getRealDir(potentialInfoFileName.c_str());
414       if (infoFileDir != fullFilename) continue;
415
416       // found infoFileName
417       infoFileName = potentialInfoFileName;
418       break;
419     }
420     PHYSFS_freeList(rc2);
421
422     // if we have an infoFile, it's an Addon
423     if (infoFileName != "")
424     {
425       try
426       {
427         std::unique_ptr<Addon> addon(new Addon(m_addons.size()));
428         addon->parse(infoFileName);
429         addon->installed = true;
430         addon->installed_physfs_filename = fileName;
431         addon->installed_absolute_filename = fullFilename;
432         addon->loaded = true;
433
434         // check if the Addon is disabled
435         if (std::find(m_ignored_addon_filenames.begin(), m_ignored_addon_filenames.end(), fileName) != m_ignored_addon_filenames.end())
436         {
437           unload(*addon);
438         }
439
440         m_addons.push_back(std::move(addon));
441       }
442       catch (const std::runtime_error& e)
443       {
444         log_warning << "Could not load add-on info for " << fullFilename << ", loading as unmanaged:" << e.what() << std::endl;
445       }
446     }
447   }
448
449   PHYSFS_freeList(rc);
450 }
451
452 /* EOF */