1 /*
2 Copyright (C) 2003 - 2008 by David White <dave@whitevine.net>
3 2008 - 2015 by Iris Morelle <shadowm2006@gmail.com>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14 */
15
16 #include "addon/manager.hpp"
17 #include "addon/manager_ui.hpp"
18 #include "filesystem.hpp"
19 #include "preferences/game.hpp"
20 #include "gettext.hpp"
21 #include "gui/dialogs/addon/connect.hpp"
22 #include "gui/dialogs/addon/manager.hpp"
23 #include "gui/dialogs/addon/uninstall_list.hpp"
24 #include "gui/dialogs/message.hpp"
25 #include "gui/dialogs/simple_item_selector.hpp"
26 #include "gui/dialogs/transient_message.hpp"
27 #include "gui/widgets/settings.hpp"
28 #include "gui/widgets/retval.hpp"
29 #include "log.hpp"
30 #include "serialization/parser.hpp"
31 #include "game_version.hpp"
32 #include "serialization/string_utils.hpp"
33 #include "addon/client.hpp"
34 #include "game_config_manager.hpp"
35
36 #include <boost/algorithm/string.hpp>
37
38 static lg::log_domain log_config("config");
39 #define ERR_CFG LOG_STREAM(err , log_config)
40 #define LOG_CFG LOG_STREAM(info, log_config)
41 #define WRN_CFG LOG_STREAM(warn, log_config)
42
43 static lg::log_domain log_filesystem("filesystem");
44 #define ERR_FS LOG_STREAM(err , log_filesystem)
45
46 static lg::log_domain log_network("network");
47 #define ERR_NET LOG_STREAM(err , log_network)
48 #define LOG_NET LOG_STREAM(info, log_network)
49
50 namespace {
get_pbl_file_path(const std::string & addon_name)51 std::string get_pbl_file_path(const std::string& addon_name)
52 {
53 const std::string& parentd = filesystem::get_addons_dir();
54 // Allow .pbl files directly in the addon dir
55 const std::string exterior = parentd + "/" + addon_name + ".pbl";
56 const std::string interior = parentd + "/" + addon_name + "/_server.pbl";
57 return filesystem::file_exists(exterior) ? exterior : interior;
58 }
59
get_info_file_path(const std::string & addon_name)60 inline std::string get_info_file_path(const std::string& addon_name)
61 {
62 return filesystem::get_addons_dir() + "/" + addon_name + "/_info.cfg";
63 }
64 }
65
have_addon_in_vcs_tree(const std::string & addon_name)66 bool have_addon_in_vcs_tree(const std::string& addon_name)
67 {
68 static const std::string parentd = filesystem::get_addons_dir();
69 return
70 filesystem::file_exists(parentd+"/"+addon_name+"/.svn") ||
71 filesystem::file_exists(parentd+"/"+addon_name+"/.git") ||
72 filesystem::file_exists(parentd+"/"+addon_name+"/.hg");
73 }
74
have_addon_pbl_info(const std::string & addon_name)75 bool have_addon_pbl_info(const std::string& addon_name)
76 {
77 return filesystem::file_exists(get_pbl_file_path(addon_name));
78 }
79
get_addon_pbl_info(const std::string & addon_name)80 config get_addon_pbl_info(const std::string& addon_name)
81 {
82 config cfg;
83 const std::string& pbl_path = get_pbl_file_path(addon_name);
84 try {
85 filesystem::scoped_istream stream = filesystem::istream_file(pbl_path);
86 read(cfg, *stream);
87 } catch(const config::error& e) {
88 throw invalid_pbl_exception(pbl_path, e.message);
89 }
90
91 return cfg;
92 }
93
set_addon_pbl_info(const std::string & addon_name,const config & cfg)94 void set_addon_pbl_info(const std::string& addon_name, const config& cfg)
95 {
96 filesystem::scoped_ostream stream = filesystem::ostream_file(get_pbl_file_path(addon_name));
97 write(*stream, cfg);
98 }
99
have_addon_install_info(const std::string & addon_name)100 bool have_addon_install_info(const std::string& addon_name)
101 {
102 return filesystem::file_exists(get_info_file_path(addon_name));
103 }
104
get_addon_install_info(const std::string & addon_name,config & cfg)105 void get_addon_install_info(const std::string& addon_name, config& cfg)
106 {
107 const std::string& info_path = get_info_file_path(addon_name);
108 filesystem::scoped_istream stream = filesystem::istream_file(info_path);
109 try {
110 read(cfg, *stream);
111 } catch(const config::error& e) {
112 ERR_CFG << "Failed to read add-on installation information for '"
113 << addon_name << "' from " << info_path << ":\n"
114 << e.message << std::endl;
115 }
116 }
117
remove_local_addon(const std::string & addon)118 bool remove_local_addon(const std::string& addon)
119 {
120 const std::string addon_dir = filesystem::get_addons_dir() + "/" + addon;
121
122 LOG_CFG << "removing local add-on: " << addon << '\n';
123
124 if(filesystem::file_exists(addon_dir) && !filesystem::delete_directory(addon_dir, true)) {
125 ERR_CFG << "Failed to delete directory/file: " << addon_dir << '\n';
126 ERR_CFG << "removal of add-on " << addon << " failed!" << std::endl;
127 return false;
128 }
129 return true;
130 }
131
available_addons()132 std::vector<std::string> available_addons()
133 {
134 std::vector<std::string> res;
135 std::vector<std::string> files, dirs;
136 const std::string parentd = filesystem::get_addons_dir();
137 filesystem::get_files_in_dir(parentd,&files,&dirs);
138
139 for(std::vector<std::string>::const_iterator i = dirs.begin(); i != dirs.end(); ++i) {
140 if (filesystem::file_exists(parentd + "/" + *i + "/_main.cfg") && have_addon_pbl_info(*i)) {
141 res.push_back(*i);
142 }
143 }
144
145 return res;
146 }
147
installed_addons()148 std::vector<std::string> installed_addons()
149 {
150 std::vector<std::string> res;
151 const std::string parentd = filesystem::get_addons_dir();
152 std::vector<std::string> files, dirs;
153 filesystem::get_files_in_dir(parentd,&files,&dirs);
154
155 for(std::vector<std::string>::const_iterator i = dirs.begin(); i != dirs.end(); ++i) {
156 if(filesystem::file_exists(parentd + "/" + *i + "/_main.cfg")) {
157 res.push_back(*i);
158 }
159 }
160
161 return res;
162 }
163
installed_addons_and_versions()164 std::map<std::string, std::string> installed_addons_and_versions()
165 {
166 std::map<std::string, std::string> addons;
167
168 for(const std::string& addon_id : installed_addons()) {
169 if(have_addon_pbl_info(addon_id)) {
170 try {
171 addons[addon_id] = get_addon_pbl_info(addon_id)["version"].str();
172 } catch(const invalid_pbl_exception& e) {
173 addons[addon_id] = "Invalid pbl file, version unknown";
174 }
175 } else if(filesystem::file_exists(get_info_file_path(addon_id))) {
176 config temp;
177 get_addon_install_info(addon_id, temp);
178 addons[addon_id] = !temp.empty() && temp.has_child("info") ? temp.child("info")["version"].str() : "Unknown";
179 } else {
180 addons[addon_id] = "Unknown";
181 }
182 }
183 return addons;
184 }
185
is_addon_installed(const std::string & addon_name)186 bool is_addon_installed(const std::string& addon_name)
187 {
188 const std::string namestem = filesystem::get_addons_dir() + "/" + addon_name;
189 return filesystem::file_exists(namestem + "/_main.cfg");
190 }
191
IsCR(const char & c)192 static inline bool IsCR(const char& c)
193 {
194 return c == '\x0D';
195 }
196
strip_cr(std::string str,bool strip)197 static std::string strip_cr(std::string str, bool strip)
198 {
199 if(!strip)
200 return str;
201 std::string::iterator new_end = std::remove_if(str.begin(), str.end(), IsCR);
202 str.erase(new_end, str.end());
203 return str;
204 }
205
read_ignore_patterns(const std::string & addon_name)206 static filesystem::blacklist_pattern_list read_ignore_patterns(const std::string& addon_name)
207 {
208 const std::string parentd = filesystem::get_addons_dir();
209 const std::string ign_file = parentd + "/" + addon_name + "/_server.ign";
210
211 filesystem::blacklist_pattern_list patterns;
212 LOG_CFG << "searching for .ign file for '" << addon_name << "'...\n";
213 if (!filesystem::file_exists(ign_file)) {
214 LOG_CFG << "no .ign file found for '" << addon_name << "'\n"
215 << "using default ignore patterns...\n";
216 return filesystem::default_blacklist;
217 }
218 LOG_CFG << "found .ign file: " << ign_file << '\n';
219 auto stream = filesystem::istream_file(ign_file);
220 std::string line;
221 while (std::getline(*stream, line)) {
222 boost::trim(line);
223 const size_t l = line.size();
224 // .gitignore & WML like comments
225 if (l == 0 || !line.compare(0,2,"# ")) continue;
226 if (line[l - 1] == '/') { // directory; we strip the last /
227 patterns.add_directory_pattern(line.substr(0, l - 1));
228 } else { // file
229 patterns.add_file_pattern(line);
230 }
231 }
232 return patterns;
233 }
234
archive_file(const std::string & path,const std::string & fname,config & cfg)235 static void archive_file(const std::string& path, const std::string& fname, config& cfg)
236 {
237 cfg["name"] = fname;
238 const bool is_cfg = (fname.size() > 4 ? (fname.substr(fname.size() - 4) == ".cfg") : false);
239 cfg["contents"] = encode_binary(strip_cr(filesystem::read_file(path + '/' + fname),is_cfg));
240 }
241
archive_dir(const std::string & path,const std::string & dirname,config & cfg,const filesystem::blacklist_pattern_list & ignore_patterns)242 static void archive_dir(const std::string& path, const std::string& dirname, config& cfg, const filesystem::blacklist_pattern_list& ignore_patterns)
243 {
244 cfg["name"] = dirname;
245 const std::string dir = path + '/' + dirname;
246
247 std::vector<std::string> files, dirs;
248 filesystem::get_files_in_dir(dir,&files,&dirs);
249 for(const std::string& name : files) {
250 bool valid = !filesystem::looks_like_pbl(name) && !ignore_patterns.match_file(name);
251 if (valid) {
252 archive_file(dir,name,cfg.add_child("file"));
253 }
254 }
255
256 for(const std::string& name : dirs) {
257 bool valid = !ignore_patterns.match_dir(name);
258 if (valid) {
259 archive_dir(dir,name,cfg.add_child("dir"),ignore_patterns);
260 }
261 }
262 }
263
archive_addon(const std::string & addon_name,config & cfg)264 void archive_addon(const std::string& addon_name, config& cfg)
265 {
266 const std::string parentd = filesystem::get_addons_dir();
267
268 filesystem::blacklist_pattern_list ignore_patterns(read_ignore_patterns(addon_name));
269 archive_dir(parentd, addon_name, cfg.add_child("dir"), ignore_patterns);
270 }
271
unarchive_file(const std::string & path,const config & cfg)272 static void unarchive_file(const std::string& path, const config& cfg)
273 {
274 filesystem::write_file(path + '/' + cfg["name"].str(), unencode_binary(cfg["contents"]));
275 }
276
unarchive_dir(const std::string & path,const config & cfg)277 static void unarchive_dir(const std::string& path, const config& cfg)
278 {
279 std::string dir;
280 if (cfg["name"].empty())
281 dir = path;
282 else
283 dir = path + '/' + cfg["name"].str();
284
285 filesystem::make_directory(dir);
286
287 for(const config &d : cfg.child_range("dir")) {
288 unarchive_dir(dir, d);
289 }
290
291 for(const config &f : cfg.child_range("file")) {
292 unarchive_file(dir, f);
293 }
294 }
295
unarchive_addon(const config & cfg)296 void unarchive_addon(const config& cfg)
297 {
298 const std::string parentd = filesystem::get_addons_dir();
299 unarchive_dir(parentd, cfg);
300 }
301
302 namespace {
303 std::map< std::string, version_info > version_info_cache;
304 } // end unnamed namespace 5
305
refresh_addon_version_info_cache()306 void refresh_addon_version_info_cache()
307 {
308 version_info_cache.clear();
309
310 LOG_CFG << "refreshing add-on versions cache\n";
311
312 const std::vector<std::string>& addons = installed_addons();
313 if(addons.empty()) {
314 return;
315 }
316
317 std::vector<std::string> addon_info_files(addons.size());
318
319 std::transform(addons.begin(), addons.end(),
320 addon_info_files.begin(), get_info_file_path);
321
322 for(size_t i = 0; i < addon_info_files.size(); ++i) {
323 assert(i < addons.size());
324
325 const std::string& addon = addons[i];
326 const std::string& info_file = addon_info_files[i];
327
328 if(filesystem::file_exists(info_file)) {
329 config cfg;
330 get_addon_install_info(addon, cfg);
331
332 const config& info_cfg = cfg.child("info");
333 if(!info_cfg) {
334 continue;
335 }
336
337 const std::string& version = info_cfg["version"].str();
338 LOG_CFG << "cached add-on version: " << addon << " [" << version << "]\n";
339
340 version_info_cache[addon] = version;
341 } else if (!have_addon_pbl_info(addon) && !have_addon_in_vcs_tree(addon)) {
342 // Don't print the warning if the user is clearly the author
343 WRN_CFG << "add-on '" << addon << "' has no _info.cfg; cannot read version info" << std::endl;
344 }
345 }
346 }
347
get_addon_version_info(const std::string & addon)348 version_info get_addon_version_info(const std::string& addon)
349 {
350 static const version_info nil;
351 std::map< std::string, version_info >::iterator entry = version_info_cache.find(addon);
352 return entry != version_info_cache.end() ? entry->second : nil;
353 }
354