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