Ported changes from 0.3.1 and bumped version to 0.3.2-SVN
[supertux.git] / src / 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 <list>
25 #include <physfs.h>
26 #include <sys/stat.h>
27 #include <stdio.h>
28 #include "addon_manager.hpp"
29 #include "config.h"
30 #include "log.hpp"
31 #include "lisp/parser.hpp"
32 #include "lisp/lisp.hpp"
33 #include "lisp/list_iterator.hpp"
34
35 #ifdef HAVE_LIBCURL
36 #include <curl/curl.h>
37 #include <curl/types.h>
38 #include <curl/easy.h>
39 #endif
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&
65 AddonManager::get_instance()
66 {
67   static AddonManager instance;
68   return instance;
69 }
70
71 AddonManager::AddonManager()
72 {
73 #ifdef HAVE_LIBCURL
74   curl_global_init(CURL_GLOBAL_ALL);
75 #endif
76 }
77
78 AddonManager::~AddonManager()
79 {
80 #ifdef HAVE_LIBCURL
81   curl_global_cleanup();
82 #endif
83 }
84
85 std::vector<Addon>
86 AddonManager::get_installed_addons() const
87 {
88   std::vector<Addon> addons;
89
90   // iterate over complete search path (i.e. directories and archives)
91   char **i = PHYSFS_getSearchPath();
92   if (!i) throw std::runtime_error("Could not query physfs search path");
93   for (; *i != NULL; i++) {
94
95     // get filename of potential archive
96     std::string fileName = *i;
97
98     // make sure it's in the writeDir
99     static const std::string writeDir = PHYSFS_getWriteDir();
100     if (fileName.compare(0, writeDir.length(), writeDir) != 0) continue;
101
102     // make sure it looks like an archive
103     static const std::string archiveExt = ".zip";
104     if (fileName.compare(fileName.length()-archiveExt.length(), archiveExt.length(), archiveExt) != 0) continue;
105
106     // make sure it exists
107     struct stat stats;
108     if (stat(fileName.c_str(), &stats) != 0) continue;
109
110     // make sure it's an actual file
111     if (!S_ISREG(stats.st_mode)) continue;
112
113     Addon addon;
114
115     // extract nice title as fallback for when the Add-on has no addoninfo file
116     static const char* dirSep = PHYSFS_getDirSeparator();
117     std::string::size_type n = fileName.rfind(dirSep) + 1;
118     if (n == std::string::npos) n = 0;
119     addon.title = fileName.substr(n, fileName.length() - n - archiveExt.length());
120     std::string shortFileName = fileName.substr(n, fileName.length() - n);
121     addon.file = shortFileName;
122    
123     // read an accompaining .nfo file, if it exists
124     static const std::string infoExt = ".nfo";
125     std::string infoFileName = fileName.substr(n, fileName.length() - n - archiveExt.length()) + infoExt;
126     if (PHYSFS_exists(infoFileName.c_str())) {
127       addon.parse(infoFileName);
128       if (addon.file != shortFileName) {
129         log_warning << "Add-on \"" << addon.title << "\", contained in file \"" << shortFileName << "\" is accompained by an addoninfo file that specifies \"" << addon.file << "\" as the Add-on's file name. Skipping." << std::endl;
130       }
131     }
132
133     // make sure the Add-on's file name does not contain weird characters
134     if (addon.file.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
135       log_warning << "Add-on \"" << addon.title << "\" contains unsafe file name. Skipping." << std::endl;
136       continue;
137     }
138
139     addon.isInstalled = true;
140     addons.push_back(addon);
141   }
142
143   return addons;
144 }
145
146 std::vector<Addon>
147 AddonManager::get_available_addons() const
148 {
149   std::vector<Addon> addons;
150
151 #ifdef HAVE_LIBCURL
152
153   char error_buffer[CURL_ERROR_SIZE+1];
154
155   const char* baseUrl = "http://supertux.berlios.de/addons/index.nfo";
156   std::string addoninfos = "";
157
158   CURL *curl_handle;
159   curl_handle = curl_easy_init();
160   curl_easy_setopt(curl_handle, CURLOPT_URL, baseUrl);
161   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
162   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_string_append);
163   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &addoninfos);
164   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
165   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
166   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
167   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
168   curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
169   CURLcode result = curl_easy_perform(curl_handle);
170   curl_easy_cleanup(curl_handle);
171
172   if (result != CURLE_OK) {
173     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
174     throw std::runtime_error("Downloading Add-on list failed: " + why);
175   }
176
177   try {
178     lisp::Parser parser;
179     std::stringstream addoninfos_stream(addoninfos);
180     const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
181
182     const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
183     if(!addons_lisp) throw std::runtime_error("Downloaded file is not an Add-on list");
184
185     lisp::ListIterator iter(addons_lisp);
186     while(iter.next()) {
187       const std::string& token = iter.item();
188       if(token == "supertux-addoninfo") {
189         Addon addon;
190         addon.parse(*(iter.lisp()));
191
192         // make sure the Add-on's file name does not contain weird characters
193         if (addon.file.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
194           log_warning << "Add-on \"" << addon.title << "\" contains unsafe file name. Skipping." << std::endl;
195           continue;
196         }
197
198         addon.isInstalled = false;
199         addons.push_back(addon);
200       } else {
201         log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
202       }
203     }
204   } catch(std::exception& e) {
205     std::stringstream msg;
206     msg << "Problem when reading Add-on list: " << e.what();
207     throw std::runtime_error(msg.str());
208   }
209
210 #endif
211
212   return addons;
213 }
214
215
216 void
217 AddonManager::install(const Addon& addon)
218 {
219   // make sure the Add-on's file name does not contain weird characters
220   if (addon.file.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
221     throw std::runtime_error("Add-on has unsafe file name (\""+addon.file+"\")");
222   }
223
224 #ifdef HAVE_LIBCURL
225
226   char error_buffer[CURL_ERROR_SIZE+1];
227
228   char* url = (char*)malloc(addon.http_url.length() + 1);
229   strncpy(url, addon.http_url.c_str(), addon.http_url.length() + 1);
230
231   PHYSFS_file* f = PHYSFS_openWrite(addon.file.c_str());
232
233   log_debug << "Downloading \"" << url << "\"" << std::endl;
234
235   CURL *curl_handle;
236   curl_handle = curl_easy_init();
237   curl_easy_setopt(curl_handle, CURLOPT_URL, url);
238   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
239   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_physfs_write);
240   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, f);
241   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
242   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
243   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
244   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
245   CURLcode result = curl_easy_perform(curl_handle);
246   curl_easy_cleanup(curl_handle);
247
248   PHYSFS_close(f);
249
250   free(url);
251
252   if (result != CURLE_OK) {
253     PHYSFS_delete(addon.file.c_str());
254     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
255     throw std::runtime_error("Downloading Add-on failed: " + why);
256   }
257
258   // write an accompaining .nfo file
259   static const std::string archiveExt = ".zip";
260   static const std::string infoExt = ".nfo";
261   std::string infoFileName = addon.file.substr(0, addon.file.length()-archiveExt.length()) + infoExt;
262   addon.write(infoFileName);
263
264   static const std::string writeDir = PHYSFS_getWriteDir();
265   static const std::string dirSep = PHYSFS_getDirSeparator();
266   std::string fullFilename = writeDir + dirSep + addon.file;
267   log_debug << "Finished downloading \"" << fullFilename << "\"" << std::endl;
268   PHYSFS_addToSearchPath(fullFilename.c_str(), 1);
269 #else
270   (void) addon;
271 #endif
272
273 }
274
275 void
276 AddonManager::remove(const Addon& addon)
277 {
278   // make sure the Add-on's file name does not contain weird characters
279   if (addon.file.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
280     throw std::runtime_error("Add-on has unsafe file name (\""+addon.file+"\")");
281   }
282
283   log_debug << "deleting file \"" << addon.file << "\"" << std::endl;
284   PHYSFS_removeFromSearchPath(addon.file.c_str());
285   PHYSFS_delete(addon.file.c_str());
286
287   // remove an accompaining .nfo file
288   static const std::string archiveExt = ".zip";
289   static const std::string infoExt = ".nfo";
290   std::string infoFileName = addon.file.substr(0, addon.file.length()-archiveExt.length()) + infoExt;
291   if (PHYSFS_exists(infoFileName.c_str())) {
292     log_debug << "deleting file \"" << infoFileName << "\"" << std::endl;
293     PHYSFS_delete(infoFileName.c_str());
294   }
295 }
296