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