1// SuperTux - Add-on Manager
2// Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
3// 2014 Ingo Ruhnke <grumbel@gmail.com>
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18#include "addon/addon_manager.hpp"
19
20#include <physfs.h>
21
22#include "addon/addon.hpp"
23#include "addon/md5.hpp"
24#include "physfs/util.hpp"
25#include "supertux/globals.hpp"
26#include "util/file_system.hpp"
27#include "util/gettext.hpp"
28#include "util/log.hpp"
29#include "util/reader.hpp"
30#include "util/reader_collection.hpp"
31#include "util/reader_document.hpp"
32#include "util/reader_mapping.hpp"
33#include "util/string_util.hpp"
34
35namespace {
36
37static const char* ADDON_INFO_PATH = "/addons/repository.nfo";
38
39MD5 md5_from_file(const std::string& filename)
40{
41 // TODO: this does not work as expected for some files -- IFileStream seems to not always behave like an ifstream.
42 //IFileStream ifs(installed_physfs_filename);
43 //std::string md5 = MD5(ifs).hex_digest();
44
45 MD5 md5;
46
47 auto file = PHYSFS_openRead(filename.c_str());
48 if (!file)
49 {
50 std::ostringstream out;
51 out << "PHYSFS_openRead() failed: " << PHYSFS_getLastErrorCode();
52 throw std::runtime_error(out.str());
53 }
54 else
55 {
56 while (true)
57 {
58 unsigned char buffer[1024];
59 PHYSFS_sint64 len = PHYSFS_readBytes(file, buffer, sizeof(buffer));
60 if (len <= 0) break;
61 md5.update(buffer, static_cast<unsigned int>(len));
62 }
63 PHYSFS_close(file);
64
65 return md5;
66 }
67}
68
69MD5 md5_from_archive(const std::string& filename)
70{
71 if (physfsutil::is_directory(filename)) {
72 return MD5();
73 } else {
74 return md5_from_file(filename);
75 }
76}
77
78static Addon& get_addon(const AddonManager::AddonList& list, const AddonId& id,
79 bool installed)
80{
81 auto it = std::find_if(list.begin(), list.end(),
82 [&id](const std::unique_ptr<Addon>& addon)
83 {
84 return addon->get_id() == id;
85 });
86
87 if (it != list.end())
88 {
89 return **it;
90 }
91 else
92 {
93 std::string type = installed ? "installed" : "repository";
94 throw std::runtime_error("Couldn't find " + type + " addon with id: " + id);
95 }
96}
97
98static std::vector<AddonId> get_addons(const AddonManager::AddonList& list)
99{
100 std::vector<AddonId> results;
101 results.reserve(list.size());
102 std::transform(list.begin(), list.end(),
103 std::back_inserter(results),
104 [](const std::unique_ptr<Addon>& addon)
105 {
106 return addon->get_id();
107 });
108 return results;
109}
110
111static PHYSFS_EnumerateCallbackResult add_to_dictionary_path(void *data, const char *origdir, const char *fname)
112{
113 std::string full_path = FileSystem::join(origdir, fname);
114 if (physfsutil::is_directory(full_path))
115 {
116 log_debug << "Adding \"" << full_path << "\" to dictionary search path" << std::endl;
117 // We want translations from addons to have precedence
118 g_dictionary_manager->add_directory(full_path, true);
119 }
120 return PHYSFS_ENUM_OK;
121}
122
123static PHYSFS_EnumerateCallbackResult remove_from_dictionary_path(void *data, const char *origdir, const char *fname)
124{
125 std::string full_path = FileSystem::join(origdir, fname);
126 if (physfsutil::is_directory(full_path))
127 {
128 g_dictionary_manager->remove_directory(full_path);
129 }
130 return PHYSFS_ENUM_OK;
131}
132} // namespace
133
134AddonManager::AddonManager(const std::string& addon_directory,
135 std::vector<Config::Addon>& addon_config) :
136 m_downloader(),
137 m_addon_directory(addon_directory),
138 m_repository_url("https://raw.githubusercontent.com/SuperTux/addons/master/index-0_6.nfo"),
139 m_addon_config(addon_config),
140 m_installed_addons(),
141 m_repository_addons(),
142 m_has_been_updated(false),
143 m_transfer_status()
144{
145 if (!PHYSFS_mkdir(m_addon_directory.c_str()))
146 {
147 std::ostringstream msg;
148 msg << "Couldn't create directory for addons '"
149 << m_addon_directory << "': " << PHYSFS_getLastErrorCode();
150 throw std::runtime_error(msg.str());
151 }
152
153 add_installed_addons();
154
155 // FIXME: We should also restore the order here
156 for (auto& addon : m_addon_config)
157 {
158 if (addon.enabled)
159 {
160 try
161 {
162 enable_addon(addon.id);
163 }
164 catch(const std::exception& err)
165 {
166 log_warning << "failed to enable addon from config: " << err.what() << std::endl;
167 }
168 }
169 }
170
171 if (PHYSFS_exists(ADDON_INFO_PATH))
172 {
173 try
174 {
175 m_repository_addons = parse_addon_infos(ADDON_INFO_PATH);
176 }
177 catch(const std::exception& err)
178 {
179 log_warning << "parsing repository.nfo failed: " << err.what() << std::endl;
180 }
181 }
182 else
183 {
184 log_info << "repository.nfo doesn't exist, not loading" << std::endl;
185 }
186
187 if (!g_config->repository_url.empty() &&
188 g_config->repository_url != m_repository_url)
189 {
190 m_repository_url = g_config->repository_url;
191 }
192}
193
194AddonManager::~AddonManager()
195{
196 // sync enabled/disabled addons into the config for saving
197 m_addon_config.clear();
198 for (const auto& addon : m_installed_addons)
199 {
200 m_addon_config.push_back({addon->get_id(), addon->is_enabled()});
201 }
202}
203
204Addon&
205AddonManager::get_repository_addon(const AddonId& id) const
206{
207 return get_addon(m_repository_addons, id, false);
208}
209
210Addon&
211AddonManager::get_installed_addon(const AddonId& id) const
212{
213 return get_addon(m_installed_addons, id, true);
214}
215
216std::vector<AddonId>
217AddonManager::get_repository_addons() const
218{
219 return get_addons(m_repository_addons);
220}
221
222
223std::vector<AddonId>
224AddonManager::get_installed_addons() const
225{
226 return get_addons(m_installed_addons);
227}
228
229bool
230AddonManager::has_online_support() const
231{
232 return true;
233}
234
235bool
236AddonManager::has_been_updated() const
237{
238 return m_has_been_updated;
239}
240
241TransferStatusPtr
242AddonManager::request_check_online()
243{
244 if (m_transfer_status)
245 {
246 throw std::runtime_error("only async request can be made to AddonManager at a time");
247 }
248 else
249 {
250 m_transfer_status = m_downloader.request_download(m_repository_url, ADDON_INFO_PATH);
251
252 m_transfer_status->then(
253 [this](bool success)
254 {
255 m_transfer_status = {};
256
257 if (success)
258 {
259 m_repository_addons = parse_addon_infos(ADDON_INFO_PATH);
260 m_has_been_updated = true;
261 }
262 });
263
264 return m_transfer_status;
265 }
266}
267
268void
269AddonManager::check_online()
270{
271 m_downloader.download(m_repository_url, ADDON_INFO_PATH);
272 m_repository_addons = parse_addon_infos(ADDON_INFO_PATH);
273 m_has_been_updated = true;
274}
275
276TransferStatusPtr
277AddonManager::request_install_addon(const AddonId& addon_id)
278{
279 if (m_transfer_status)
280 {
281 throw std::runtime_error("only one addon install request allowed at a time");
282 }
283 else
284 {
285 { // remove addon if it already exists
286 auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
287 [&addon_id](const std::unique_ptr<Addon>& addon)
288 {
289 return addon->get_id() == addon_id;
290 });
291 if (it != m_installed_addons.end())
292 {
293 log_debug << "reinstalling addon " << addon_id << std::endl;
294 if ((*it)->is_enabled())
295 {
296 disable_addon((*it)->get_id());
297 }
298 m_installed_addons.erase(it);
299 }
300 else
301 {
302 log_debug << "installing addon " << addon_id << std::endl;
303 }
304 }
305
306 auto& addon = get_repository_addon(addon_id);
307
308 std::string install_filename = FileSystem::join(m_addon_directory, addon.get_filename());
309
310 m_transfer_status = m_downloader.request_download(addon.get_url(), install_filename);
311
312 m_transfer_status->then(
313 [this, install_filename, addon_id](bool success)
314 {
315 m_transfer_status = {};
316
317 if (success)
318 {
319 // complete the addon install
320 Addon& repository_addon = get_repository_addon(addon_id);
321
322 MD5 md5 = md5_from_file(install_filename);
323 if (repository_addon.get_md5() != md5.hex_digest())
324 {
325 if (PHYSFS_delete(install_filename.c_str()) == 0)
326 {
327 log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastErrorCode() << std::endl;
328 }
329
330 throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
331 }
332 else
333 {
334 const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
335 if (!realdir)
336 {
337 throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
338 }
339 else
340 {
341 add_installed_archive(install_filename, md5.hex_digest());
342 }
343 }
344 }
345 });
346
347 return m_transfer_status;
348 }
349}
350
351void
352AddonManager::install_addon(const AddonId& addon_id)
353{
354 { // remove addon if it already exists
355 auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
356 [&addon_id](const std::unique_ptr<Addon>& addon)
357 {
358 return addon->get_id() == addon_id;
359 });
360 if (it != m_installed_addons.end())
361 {
362 log_debug << "reinstalling addon " << addon_id << std::endl;
363 if ((*it)->is_enabled())
364 {
365 disable_addon((*it)->get_id());
366 }
367 m_installed_addons.erase(it);
368 }
369 else
370 {
371 log_debug << "installing addon " << addon_id << std::endl;
372 }
373 }
374
375 auto& repository_addon = get_repository_addon(addon_id);
376
377 std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
378
379 m_downloader.download(repository_addon.get_url(), install_filename);
380
381 MD5 md5 = md5_from_file(install_filename);
382 if (repository_addon.get_md5() != md5.hex_digest())
383 {
384 if (PHYSFS_delete(install_filename.c_str()) == 0)
385 {
386 log_warning << "PHYSFS_delete failed: " << PHYSFS_getLastErrorCode() << std::endl;
387 }
388
389 throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
390 }
391 else
392 {
393 const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
394 if (!realdir)
395 {
396 throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
397 }
398 else
399 {
400 add_installed_archive(install_filename, md5.hex_digest());
401 }
402 }
403}
404
405void
406AddonManager::uninstall_addon(const AddonId& addon_id)
407{
408 log_debug << "uninstalling addon " << addon_id << std::endl;
409 auto& addon = get_installed_addon(addon_id);
410 if (addon.is_enabled())
411 {
412 disable_addon(addon_id);
413 }
414 log_debug << "deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
415 PHYSFS_delete(addon.get_install_filename().c_str());
416 m_installed_addons.erase(std::remove_if(m_installed_addons.begin(), m_installed_addons.end(),
417 [&addon](const std::unique_ptr<Addon>& rhs)
418 {
419 return addon.get_id() == rhs->get_id();
420 }),
421 m_installed_addons.end());
422}
423
424void
425AddonManager::enable_addon(const AddonId& addon_id)
426{
427 log_debug << "enabling addon " << addon_id << std::endl;
428 auto& addon = get_installed_addon(addon_id);
429 if (addon.is_enabled())
430 {
431 log_warning << "Tried enabling already enabled Add-on" << std::endl;
432 }
433 else
434 {
435 log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
436 //int PHYSFS_mount(addon.installed_install_filename.c_str(), "addons/", 0)
437
438 std::string mountpoint;
439 switch (addon.get_format()) {
440 case Addon::ORIGINAL:
441 mountpoint = "";
442 break;
443 default:
444 mountpoint = "custom/" + addon_id;
445 break;
446 }
447
448 if (PHYSFS_mount(addon.get_install_filename().c_str(), mountpoint.c_str(), 0) == 0)
449 {
450 log_warning << "Could not add " << addon.get_install_filename() << " to search path: "
451 << PHYSFS_getLastErrorCode() << std::endl;
452 }
453 else
454 {
455 if (addon.get_type() == Addon::LANGUAGEPACK)
456 {
457 PHYSFS_enumerate(addon.get_id().c_str(), add_to_dictionary_path, nullptr);
458 }
459 addon.set_enabled(true);
460 }
461 }
462}
463
464void
465AddonManager::disable_addon(const AddonId& addon_id)
466{
467 log_debug << "disabling addon " << addon_id << std::endl;
468 auto& addon = get_installed_addon(addon_id);
469 if (!addon.is_enabled())
470 {
471 log_warning << "Tried disabling already disabled Add-On" << std::endl;
472 }
473 else
474 {
475 log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
476 if (PHYSFS_unmount(addon.get_install_filename().c_str()) == 0)
477 {
478 log_warning << "Could not remove " << addon.get_install_filename() << " from search path: "
479 << PHYSFS_getLastErrorCode() << std::endl;
480 }
481 else
482 {
483 if (addon.get_type() == Addon::LANGUAGEPACK)
484 {
485 PHYSFS_enumerate(addon.get_id().c_str(), remove_from_dictionary_path, nullptr);
486 }
487 addon.set_enabled(false);
488 }
489 }
490}
491
492bool
493AddonManager::is_old_enabled_addon(const std::unique_ptr<Addon>& addon) const
494{
495 return addon->get_format() == Addon::ORIGINAL &&
496 addon->get_type() != Addon::LANGUAGEPACK &&
497 addon->is_enabled();
498}
499
500bool
501AddonManager::is_old_addon_enabled() const {
502 auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
503 [this](const std::unique_ptr<Addon>& addon)
504 {
505 return is_old_enabled_addon(addon);
506 });
507
508 return it != m_installed_addons.end();
509}
510
511void
512AddonManager::disable_old_addons()
513{
514 for (auto& addon : m_installed_addons) {
515 if (is_old_enabled_addon(addon)) {
516 disable_addon(addon->get_id());
517 }
518 }
519}
520
521void
522AddonManager::mount_old_addons()
523{
524 std::string mountpoint;
525 for (auto& addon : m_installed_addons) {
526 if (is_old_enabled_addon(addon)) {
527 if (PHYSFS_mount(addon->get_install_filename().c_str(), mountpoint.c_str(), 0) == 0)
528 {
529 log_warning << "Could not add " << addon->get_install_filename() << " to search path: "
530 << PHYSFS_getLastErrorCode() << std::endl;
531 }
532 }
533 }
534}
535
536void
537AddonManager::unmount_old_addons()
538{
539 for (auto& addon : m_installed_addons) {
540 if (is_old_enabled_addon(addon)) {
541 if (PHYSFS_unmount(addon->get_install_filename().c_str()) == 0)
542 {
543 log_warning << "Could not remove " << addon->get_install_filename() << " from search path: "
544 << PHYSFS_getLastErrorCode() << std::endl;
545 }
546 }
547 }
548}
549
550bool
551AddonManager::is_from_old_addon(const std::string& filename) const
552{
553 std::string real_path = PHYSFS_getRealDir(filename.c_str());
554 for (auto& addon : m_installed_addons) {
555 if (is_old_enabled_addon(addon) &&
556 addon->get_install_filename() == real_path) {
557 return true;
558 }
559 }
560 return false;
561}
562
563bool
564AddonManager::is_addon_installed(const std::string& id) const
565{
566 return std::any_of(get_installed_addons().begin(), get_installed_addons().end(),
567 [id] (const auto& installed_addon) {
568 return installed_addon == id;
569 });
570}
571
572std::vector<std::string>
573AddonManager::scan_for_archives() const
574{
575 std::vector<std::string> archives;
576
577 // Search for archives and add them to the search path
578 std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
579 rc(PHYSFS_enumerateFiles(m_addon_directory.c_str()),
580 PHYSFS_freeList);
581 for (char** i = rc.get(); *i != nullptr; ++i)
582 {
583 const std::string fullpath = FileSystem::join(m_addon_directory, *i);
584 if (physfsutil::is_directory(fullpath))
585 {
586 // ignore dot files (e.g. '.git/')
587 if ((*i)[0] != '.') {
588 archives.push_back(fullpath);
589 }
590 }
591 else
592 {
593 if (StringUtil::has_suffix(StringUtil::tolower(*i), ".zip")) {
594 if (PHYSFS_exists(fullpath.c_str())) {
595 archives.push_back(fullpath);
596 }
597 }
598 }
599 }
600
601 return archives;
602}
603
604std::string
605AddonManager::scan_for_info(const std::string& archive_os_path) const
606{
607 std::unique_ptr<char*, decltype(&PHYSFS_freeList)>
608 rc2(PHYSFS_enumerateFiles("/"),
609 PHYSFS_freeList);
610 for (char** j = rc2.get(); *j != nullptr; ++j)
611 {
612 if (StringUtil::has_suffix(*j, ".nfo"))
613 {
614 std::string nfo_filename = FileSystem::join("/", *j);
615
616 // make sure it's in the current archive_os_path
617 const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
618 if (!realdir)
619 {
620 log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << PHYSFS_getLastErrorCode() << std::endl;
621 }
622 else
623 {
624 if (realdir == archive_os_path)
625 {
626 return nfo_filename;
627 }
628 }
629 }
630 }
631
632 return std::string();
633}
634
635void
636AddonManager::add_installed_archive(const std::string& archive, const std::string& md5)
637{
638 const char* realdir = PHYSFS_getRealDir(archive.c_str());
639 if (!realdir)
640 {
641 log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
642 << PHYSFS_getLastErrorCode() << std::endl;
643 }
644 else
645 {
646 std::string os_path = FileSystem::join(realdir, archive);
647
648 PHYSFS_mount(os_path.c_str(), nullptr, 0);
649
650 std::string nfo_filename = scan_for_info(os_path);
651
652 if (nfo_filename.empty())
653 {
654 log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
655 }
656 else
657 {
658 try
659 {
660 std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
661 addon->set_install_filename(os_path, md5);
662 m_installed_addons.push_back(std::move(addon));
663 }
664 catch (const std::runtime_error& e)
665 {
666 log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
667 }
668 }
669
670 PHYSFS_unmount(os_path.c_str());
671 }
672}
673
674void
675AddonManager::add_installed_addons()
676{
677 auto archives = scan_for_archives();
678
679 for (const auto& archive : archives)
680 {
681 MD5 md5 = md5_from_archive(archive);
682 add_installed_archive(archive, md5.hex_digest());
683 }
684}
685
686AddonManager::AddonList
687AddonManager::parse_addon_infos(const std::string& filename) const
688{
689 AddonList m_addons;
690
691 try
692 {
693 register_translation_directory(filename);
694 auto doc = ReaderDocument::from_file(filename);
695 auto root = doc.get_root();
696 if (root.get_name() != "supertux-addons")
697 {
698 throw std::runtime_error("Downloaded file is not an Add-on list");
699 }
700 else
701 {
702 auto addon_collection = root.get_collection();
703 for (auto const& addon_node : addon_collection.get_objects())
704 {
705 if (addon_node.get_name() != "supertux-addoninfo")
706 {
707 log_warning << "Unknown token '" << addon_node.get_name() << "' in Add-on list" << std::endl;
708 }
709 else
710 {
711 try
712 {
713 std::unique_ptr<Addon> addon = Addon::parse(addon_node.get_mapping());
714 m_addons.push_back(std::move(addon));
715 }
716 catch(const std::exception& e)
717 {
718 log_warning << "Problem when reading Add-on entry: " << e.what() << std::endl;
719 }
720 }
721 }
722
723 return m_addons;
724 }
725 }
726 catch(const std::exception& e)
727 {
728 std::stringstream msg;
729 msg << "Problem when reading Add-on list: " << e.what();
730 throw std::runtime_error(msg.str());
731 }
732}
733
734void
735AddonManager::update()
736{
737 m_downloader.update();
738}
739
740void
741AddonManager::check_for_langpack_updates()
742{
743 const std::string& language = g_dictionary_manager->get_language().get_language();
744 if (language == "en")
745 return;
746
747 try
748 {
749 check_online();
750 try
751 {
752 const std::string& addon_id = "language-pack";
753 log_debug << "Looking for language addon with ID " << addon_id << "..." << std::endl;
754 Addon& langpack = get_repository_addon(addon_id);
755
756 try
757 {
758 auto& installed_langpack = get_installed_addon(addon_id);
759 if (installed_langpack.get_md5() == langpack.get_md5() ||
760 installed_langpack.get_version() > langpack.get_version())
761 {
762 log_debug << "Language addon " << addon_id << " is already the latest version." << std::endl;
763 return;
764 }
765
766 // Langpack update available. Let's install it!
767 install_addon(addon_id);
768 enable_addon(addon_id);
769 }
770 catch(const std::exception&)
771 {
772 log_debug << "Language addon " << addon_id << " is not installed. Installing..." << std::endl;
773 install_addon(addon_id);
774 enable_addon(addon_id);
775 }
776 }
777 catch(std::exception&)
778 {
779 log_debug << "Language addon for current locale not found." << std::endl;
780 }
781 }
782 catch(...)
783 {
784 // If anything fails here, just silently ignore.
785 }
786}
787
788/* EOF */
789