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