3 // SuperTux - Add-on Manager
4 // Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
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.
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.
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
30 #include "addon/addon_manager.hpp"
31 #include "addon/addon.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"
41 #include <curl/curl.h>
42 #include <curl/types.h>
43 #include <curl/easy.h>
49 size_t my_curl_string_append(void *ptr, size_t size, size_t nmemb, void *string_ptr)
51 std::string& s = *static_cast<std::string*>(string_ptr);
52 std::string buf(static_cast<char*>(ptr), size * nmemb);
54 log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
58 size_t my_curl_physfs_write(void *ptr, size_t size, size_t nmemb, void *f_p)
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;
70 AddonManager::get_instance()
72 static AddonManager instance;
76 AddonManager::AddonManager()
79 curl_global_init(CURL_GLOBAL_ALL);
83 AddonManager::~AddonManager()
86 curl_global_cleanup();
89 for (std::vector<Addon*>::iterator i = addons.begin(); i != addons.end(); i++) delete *i;
93 AddonManager::get_addons()
96 for (std::vector<Addon>::iterator it = installed_addons.begin(); it != installed_addons.end(); ++it) {
98 if (addon.md5 == "") addon.md5 = calculate_md5(addon);
105 AddonManager::check_online()
108 char error_buffer[CURL_ERROR_SIZE+1];
110 const char* baseUrl = "http://supertux.berlios.de/addons/index.nfo";
111 std::string addoninfos = "";
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);
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);
134 std::stringstream addoninfos_stream(addoninfos);
135 const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
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");
140 lisp::ListIterator iter(addons_lisp);
142 const std::string& token = iter.item();
143 if(token != "supertux-addoninfo") {
144 log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
147 Addon* addon_ptr = new Addon();
148 Addon& addon = *addon_ptr;
149 addon.parse(*(iter.lisp()));
150 addon.installed = false;
151 addon.loaded = false;
153 // make sure the list of known Add-ons does not already contain this one
155 for (std::vector<Addon*>::const_iterator i = addons.begin(); i != addons.end(); i++) {
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;
173 addons.push_back(addon_ptr);
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());
186 AddonManager::install(Addon* addon)
190 if (addon->installed) throw std::runtime_error("Tried installing installed Add-on");
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+"\")");
197 std::string fileName = addon->suggested_filename;
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+"\")");
207 char error_buffer[CURL_ERROR_SIZE+1];
209 char* url = (char*)malloc(addon->http_url.length() + 1);
210 strncpy(url, addon->http_url.c_str(), addon->http_url.length() + 1);
212 PHYSFS_file* f = PHYSFS_openWrite(fileName.c_str());
214 log_debug << "Downloading \"" << url << "\"" << std::endl;
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);
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);
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;
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);
253 log_debug << "Finished downloading \"" << addon->installed_absolute_filename << "\". Enabling Add-on." << std::endl;
264 AddonManager::remove(Addon* addon)
266 if (!addon->installed) throw std::runtime_error("Tried removing non-installed Add-on");
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+"\")");
277 log_debug << "deleting file \"" << addon->installed_absolute_filename << "\"" << std::endl;
278 PHYSFS_delete(addon->installed_absolute_filename.c_str());
279 addon->installed = false;
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
285 AddonManager::disable(Addon* addon)
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);
296 AddonManager::enable(Addon* addon)
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);
308 AddonManager::unload(Addon* addon)
310 if (!addon->installed) throw std::runtime_error("Tried unloading non-installed Add-on");
311 if (!addon->loaded) return;
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;
319 addon->loaded = false;
323 AddonManager::load(Addon* addon)
325 if (!addon->installed) throw std::runtime_error("Tried loading non-installed Add-on");
326 if (addon->loaded) return;
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;
334 addon->loaded = true;
338 AddonManager::load_addons()
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);
347 // Search for archives and add them to the search path
348 char** rc = PHYSFS_enumerateFiles("/");
350 for(char** i = rc; *i != 0; ++i) {
352 // get filename of potential archive
353 std::string fileName = *i;
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;
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;
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;
369 // make sure it exists
371 if (stat(fullFilename.c_str(), &stats) != 0) continue;
373 // make sure it's an actual file
374 if (!S_ISREG(stats.st_mode)) continue;
376 log_debug << "Found archive \"" << fullFilename << "\"" << std::endl;
378 // add archive to search path
379 PHYSFS_addToSearchPath(fullFilename.c_str(), 0);
381 // Search for infoFiles
382 std::string infoFileName = "";
383 char** rc2 = PHYSFS_enumerateFiles("/");
384 for(char** i = rc2; *i != 0; ++i) {
386 // get filename of potential infoFile
387 std::string potentialInfoFileName = *i;
389 // make sure it looks like an infoFile
390 static const std::string infoExt = ".nfo";
391 if (potentialInfoFileName.length() <= infoExt.length())
394 if (potentialInfoFileName.compare(potentialInfoFileName.length()-infoExt.length(), infoExt.length(), infoExt) != 0)
397 // make sure it's in the current archive
398 std::string infoFileDir = PHYSFS_getRealDir(potentialInfoFileName.c_str());
399 if (infoFileDir != fullFilename) continue;
401 // found infoFileName
402 infoFileName = potentialInfoFileName;
405 PHYSFS_freeList(rc2);
407 // if we have an infoFile, it's an Addon
408 if (infoFileName != "") {
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);
418 // check if the Addon is disabled
419 if (std::find(ignored_addon_filenames.begin(), ignored_addon_filenames.end(), fileName) != ignored_addon_filenames.end()) {
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;
435 AddonManager::read_config(const lisp::Lisp& lisp)
437 lisp.get_vector("disabled-addons", ignored_addon_filenames);
441 AddonManager::write_config(lisp::Writer& writer)
443 writer.write_string_vector("disabled-addons", ignored_addon_filenames);