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