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