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