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