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