1 // SuperTux - Add-on Manager
2 // Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
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.
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.
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/>.
17 #include "addon/addon_manager.hpp"
30 # include <curl/curl.h>
31 # include <curl/easy.h>
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"
44 size_t my_curl_string_append(void *ptr, size_t size, size_t nmemb, void *string_ptr)
46 std::string& s = *static_cast<std::string*>(string_ptr);
47 std::string buf(static_cast<char*>(ptr), size * nmemb);
49 log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
53 size_t my_curl_physfs_write(void *ptr, size_t size, size_t nmemb, void *f_p)
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;
64 AddonManager::AddonManager(std::vector<std::string>& ignored_addon_filenames) :
66 m_ignored_addon_filenames(ignored_addon_filenames)
69 curl_global_init(CURL_GLOBAL_ALL);
73 AddonManager::~AddonManager()
76 curl_global_cleanup();
81 AddonManager::get_addon(int id)
83 if (0 <= id && id < static_cast<int>(m_addons.size()))
89 throw std::runtime_error("AddonManager::get_addon(): id out of range: " + std::to_string(id));
93 const std::vector<std::unique_ptr<Addon> >&
94 AddonManager::get_addons() const
97 for (std::vector<Addon>::iterator it = installed_addons.begin(); it != installed_addons.end(); ++it) {
99 if (addon.md5 == "") addon.md5 = calculate_md5(addon);
106 AddonManager::has_online_support() const
116 AddonManager::check_online()
119 char error_buffer[CURL_ERROR_SIZE+1];
121 const char* baseUrl = "http://addons.supertux.googlecode.com/git/index-0_3_5.nfo";
122 std::string addoninfos = "";
125 curl_handle = curl_easy_init();
126 curl_easy_setopt(curl_handle, CURLOPT_URL, baseUrl);
127 curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
128 curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_string_append);
129 curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &addoninfos);
130 curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
131 curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
132 curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
133 curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
134 curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
135 CURLcode result = curl_easy_perform(curl_handle);
136 curl_easy_cleanup(curl_handle);
138 if (result != CURLE_OK) {
139 std::string why = error_buffer[0] ? error_buffer : "unhandled error";
140 throw std::runtime_error("Downloading Add-on list failed: " + why);
145 std::stringstream addoninfos_stream(addoninfos);
146 const lisp::Lisp* root = parser.parse(addoninfos_stream, "supertux-addons");
148 const lisp::Lisp* addons_lisp = root->get_lisp("supertux-addons");
149 if(!addons_lisp) throw std::runtime_error("Downloaded file is not an Add-on list");
151 lisp::ListIterator iter(addons_lisp);
154 const std::string& token = iter.item();
155 if(token != "supertux-addoninfo")
157 log_warning << "Unknown token '" << token << "' in Add-on list" << std::endl;
160 std::unique_ptr<Addon> addon(new Addon(m_addons.size()));
161 addon->parse(*(iter.lisp()));
162 addon->installed = false;
163 addon->loaded = false;
165 // make sure the list of known Add-ons does not already contain this one
167 for (auto i = m_addons.begin(); i != m_addons.end(); ++i) {
178 else if (addon->suggested_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos)
180 // make sure the Add-on's file name does not contain weird characters
181 log_warning << "Add-on \"" << addon->title << "\" contains unsafe file name. Skipping." << std::endl;
185 m_addons.push_back(std::move(addon));
188 } catch(std::exception& e) {
189 std::stringstream msg;
190 msg << "Problem when reading Add-on list: " << e.what();
191 throw std::runtime_error(msg.str());
198 AddonManager::install(Addon& addon)
202 if (addon.installed) throw std::runtime_error("Tried installing installed Add-on");
204 // make sure the Add-on's file name does not contain weird characters
205 if (addon.suggested_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
206 throw std::runtime_error("Add-on has unsafe file name (\""+addon.suggested_filename+"\")");
209 std::string fileName = addon.suggested_filename;
211 // make sure its file doesn't already exist
212 if (PHYSFS_exists(fileName.c_str())) {
213 fileName = addon.stored_md5 + "_" + addon.suggested_filename;
214 if (PHYSFS_exists(fileName.c_str())) {
215 throw std::runtime_error("Add-on of suggested filename already exists (\""+addon.suggested_filename+"\", \""+fileName+"\")");
219 char error_buffer[CURL_ERROR_SIZE+1];
221 char* url = (char*)malloc(addon.http_url.length() + 1);
222 strncpy(url, addon.http_url.c_str(), addon.http_url.length() + 1);
224 PHYSFS_file* f = PHYSFS_openWrite(fileName.c_str());
226 log_debug << "Downloading \"" << url << "\"" << std::endl;
229 curl_handle = curl_easy_init();
230 curl_easy_setopt(curl_handle, CURLOPT_URL, url);
231 curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
232 curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, my_curl_physfs_write);
233 curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, f);
234 curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
235 curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
236 curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
237 curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
238 CURLcode result = curl_easy_perform(curl_handle);
239 curl_easy_cleanup(curl_handle);
245 if (result != CURLE_OK) {
246 PHYSFS_delete(fileName.c_str());
247 std::string why = error_buffer[0] ? error_buffer : "unhandled error";
248 throw std::runtime_error("Downloading Add-on failed: " + why);
251 addon.installed = true;
252 addon.installed_physfs_filename = fileName;
253 static const std::string writeDir = PHYSFS_getWriteDir();
254 static const std::string dirSep = PHYSFS_getDirSeparator();
255 addon.installed_absolute_filename = writeDir + dirSep + fileName;
256 addon.loaded = false;
258 if (addon.get_md5() != addon.stored_md5) {
259 addon.installed = false;
260 PHYSFS_delete(fileName.c_str());
261 std::string why = "MD5 checksums differ";
262 throw std::runtime_error("Downloading Add-on failed: " + why);
265 log_debug << "Finished downloading \"" << addon.installed_absolute_filename << "\". Enabling Add-on." << std::endl;
276 AddonManager::remove(Addon& addon)
278 if (!addon.installed) throw std::runtime_error("Tried removing non-installed Add-on");
282 // make sure the Add-on's file name does not contain weird characters
283 if (addon.installed_physfs_filename.find_first_not_of("match.quiz-proxy_gwenblvdjfks0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ") != std::string::npos) {
284 throw std::runtime_error("Add-on has unsafe file name (\""+addon.installed_physfs_filename+"\")");
289 log_debug << "deleting file \"" << addon.installed_absolute_filename << "\"" << std::endl;
290 PHYSFS_delete(addon.installed_absolute_filename.c_str());
291 addon.installed = false;
293 // FIXME: As we don't know anything more about it (e.g. where to get it), remove it from list of known Add-ons
297 AddonManager::disable(Addon& addon)
301 std::string fileName = addon.installed_physfs_filename;
302 if (std::find(m_ignored_addon_filenames.begin(), m_ignored_addon_filenames.end(), fileName) == m_ignored_addon_filenames.end()) {
303 m_ignored_addon_filenames.push_back(fileName);
308 AddonManager::enable(Addon& addon)
312 std::string fileName = addon.installed_physfs_filename;
313 std::vector<std::string>::iterator i = std::find(m_ignored_addon_filenames.begin(), m_ignored_addon_filenames.end(), fileName);
314 if (i != m_ignored_addon_filenames.end()) {
315 m_ignored_addon_filenames.erase(i);
320 AddonManager::unload(Addon& addon)
322 if (!addon.installed) throw std::runtime_error("Tried unloading non-installed Add-on");
323 if (!addon.loaded) return;
325 log_debug << "Removing archive \"" << addon.installed_absolute_filename << "\" from search path" << std::endl;
326 if (PHYSFS_removeFromSearchPath(addon.installed_absolute_filename.c_str()) == 0) {
327 log_warning << "Could not remove " << addon.installed_absolute_filename << " from search path. Ignoring." << std::endl;
331 addon.loaded = false;
335 AddonManager::load(Addon& addon)
337 if (!addon.installed) throw std::runtime_error("Tried loading non-installed Add-on");
338 if (addon.loaded) return;
340 log_debug << "Adding archive \"" << addon.installed_absolute_filename << "\" to search path" << std::endl;
341 if (PHYSFS_addToSearchPath(addon.installed_absolute_filename.c_str(), 0) == 0) {
342 log_warning << "Could not add " << addon.installed_absolute_filename << " to search path. Ignoring." << std::endl;
350 AddonManager::load_addons()
352 // unload all Addons and forget about them
353 for (auto i = m_addons.begin(); i != m_addons.end(); ++i)
355 if ((*i)->installed && (*i)->loaded)
362 // Search for archives and add them to the search path
363 char** rc = PHYSFS_enumerateFiles("/");
365 for(char** i = rc; *i != 0; ++i) {
367 // get filename of potential archive
368 std::string fileName = *i;
370 const std::string archiveDir = PHYSFS_getRealDir(fileName.c_str());
371 static const std::string dirSep = PHYSFS_getDirSeparator();
372 std::string fullFilename = archiveDir + dirSep + fileName;
375 // make sure it's in the writeDir
376 static const std::string writeDir = PHYSFS_getWriteDir();
377 if (fileName.compare(0, writeDir.length(), writeDir) != 0) continue;
380 // make sure it looks like an archive
381 static const std::string archiveExt = ".zip";
382 if (fullFilename.compare(fullFilename.length()-archiveExt.length(), archiveExt.length(), archiveExt) != 0) continue;
384 // make sure it exists
386 if (stat(fullFilename.c_str(), &stats) != 0) continue;
388 // make sure it's an actual file
389 if (!S_ISREG(stats.st_mode)) continue;
391 log_debug << "Found archive \"" << fullFilename << "\"" << std::endl;
393 // add archive to search path
394 PHYSFS_addToSearchPath(fullFilename.c_str(), 0);
396 // Search for infoFiles
397 std::string infoFileName = "";
398 char** rc2 = PHYSFS_enumerateFiles("/");
399 for(char** j = rc2; *j != 0; ++j) {
401 // get filename of potential infoFile
402 std::string potentialInfoFileName = *j;
404 // make sure it looks like an infoFile
405 static const std::string infoExt = ".nfo";
406 if (potentialInfoFileName.length() <= infoExt.length())
409 if (potentialInfoFileName.compare(potentialInfoFileName.length()-infoExt.length(), infoExt.length(), infoExt) != 0)
412 // make sure it's in the current archive
413 std::string infoFileDir = PHYSFS_getRealDir(potentialInfoFileName.c_str());
414 if (infoFileDir != fullFilename) continue;
416 // found infoFileName
417 infoFileName = potentialInfoFileName;
420 PHYSFS_freeList(rc2);
422 // if we have an infoFile, it's an Addon
423 if (infoFileName != "")
427 std::unique_ptr<Addon> addon(new Addon(m_addons.size()));
428 addon->parse(infoFileName);
429 addon->installed = true;
430 addon->installed_physfs_filename = fileName;
431 addon->installed_absolute_filename = fullFilename;
432 addon->loaded = true;
434 // check if the Addon is disabled
435 if (std::find(m_ignored_addon_filenames.begin(), m_ignored_addon_filenames.end(), fileName) != m_ignored_addon_filenames.end())
440 m_addons.push_back(std::move(addon));
442 catch (const std::runtime_error& e)
444 log_warning << "Could not load add-on info for " << fullFilename << ", loading as unmanaged:" << e.what() << std::endl;