The Battle for Wesnoth  1.19.23+dev
manager_ui.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2025
3  by Iris Morelle <shadowm2006@gmail.com>
4  Copyright (C) 2003 - 2008 by David White <dave@whitevine.net>
5  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6 
7  This program is free software; you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation; either version 2 of the License, or
10  (at your option) any later version.
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY.
13 
14  See the COPYING file for more details.
15 */
16 
17 #include "addon/manager_ui.hpp"
18 
19 #include "addon/client.hpp"
20 #include "addon/info.hpp"
21 #include "addon/manager.hpp"
22 #include "config_cache.hpp"
23 #include "filesystem.hpp"
24 #include "formula/string_utils.hpp"
26 #include "gettext.hpp"
30 #include "gui/dialogs/message.hpp"
32 #include "gui/widgets/retval.hpp"
33 #include "log.hpp"
34 #include "wml_exception.hpp"
35 
36 static lg::log_domain log_config("config");
37 static lg::log_domain log_network("network");
38 static lg::log_domain log_filesystem("filesystem");
39 static lg::log_domain log_addons_client("addons-client");
40 
41 #define ERR_CFG LOG_STREAM(err, log_config)
42 #define INFO_CFG LOG_STREAM(info, log_config)
43 
44 #define ERR_NET LOG_STREAM(err, log_network)
45 
46 #define ERR_FS LOG_STREAM(err, log_filesystem)
47 
48 #define LOG_AC LOG_STREAM(info, log_addons_client)
49 
50 
51 namespace {
52 
53 std::string format_remote_disconnect_message(const network_asio::error& e)
54 {
55  std::string message = _("Remote host disconnected.");
56  const std::string details = e.what();
57 
58  if(details.empty()) {
59  return message;
60  }
61 
62  message += "\n\n";
63  message += _("Raw network error:");
64  message += "\n";
65  message += details;
66  return message;
67 }
68 
69 bool get_addons_list(addons_client& client, addons_list& list)
70 {
71  list.clear();
72 
73  config cfg;
74  client.request_addons_list(cfg, prefs::get().addon_icons());
75 
76  read_addons_list(cfg, list);
77 
78  return true;
79 }
80 
81 bool addons_manager_ui(const std::string& remote_address)
82 {
83  bool need_wml_cache_refresh = false;
84 
85  prefs::get().set_campaign_server(remote_address);
86 
87  try {
88  addons_client client(remote_address);
89  client.connect();
90 
91  gui2::dialogs::addon_manager dlg(client);
92  dlg.show();
93 
94  need_wml_cache_refresh = dlg.get_need_wml_cache_refresh();
95  } catch(const config::error& e) {
96  ERR_CFG << "config::error thrown during transaction with add-on server; \""<< e.message << "\"";
97  gui2::show_error_message(_("Network communication error."));
98  } catch(const network_asio::error& e) {
99  ERR_NET << "network_asio::error thrown during transaction with add-on server; \""<< e.what() << "\"";
100  gui2::show_error_message(format_remote_disconnect_message(e));
101  } catch(const filesystem::io_exception& e) {
102  ERR_FS << "filesystem::io_exception thrown while installing an addon; \"" << e.what() << "\"";
103  gui2::show_error_message(_("A problem occurred when trying to create the files necessary to install this add-on."));
104  } catch(const invalid_pbl_exception& e) {
105  ERR_CFG << "could not read .pbl file " << e.path << ": " << e.message;
106 
107  utils::string_map symbols;
108  symbols["path"] = e.path;
109  symbols["msg"] = e.message;
110 
112  VGETTEXT("A local file with add-on publishing information could not be read.\n\nFile: $path\nError message: $msg", symbols));
113  } catch(const wml_exception& e) {
114  e.show();
115  } catch(const addons_client::user_exit&) {
116  LOG_AC << "initial connection canceled by user";
117  } catch(const addons_client::user_disconnect&) {
118  LOG_AC << "attempt to reconnect canceled by user";
119  } catch(const addons_client::invalid_server_address&) {
120  gui2::show_error_message(_("The add-ons server address specified is not valid."));
121  }
122 
123  return need_wml_cache_refresh;
124 }
125 
126 bool uninstall_local_addons()
127 {
128  const std::string list_lead = "\n\n";
129 
130  const std::vector<std::string>& addons = installed_addons();
131 
132  if(addons.empty()) {
133  gui2::show_error_message(_("You have no add-ons installed."));
134  return false;
135  }
136 
137  std::map<std::string, std::string> addon_titles_map;
138 
139  for(const std::string& id : addons) {
140  std::string title;
141 
142  if(have_addon_install_info(id)) {
143  // _info.cfg may have the add-on's title starting with 1.11.7,
144  // if the add-on was downloading using the revised _info.cfg writer.
145  config info_cfg;
146  get_addon_install_info(id, info_cfg);
147 
148  if(!info_cfg.empty()) {
149  title = info_cfg["title"].str();
150  }
151  }
152 
153  if(title.empty()) {
154  // Transform the id into a title as a last resort.
155  title = make_addon_title(id);
156  }
157 
158  addon_titles_map[id] = title;
159  }
160 
161  int res;
162 
163  std::vector<std::string> remove_ids;
164  std::set<std::string> remove_names;
165 
166  do {
167  gui2::dialogs::addon_uninstall_list dlg(addon_titles_map);
168  dlg.show();
169 
170  remove_ids = dlg.selected_addons();
171  if(remove_ids.empty()) {
172  return false;
173  }
174 
175  remove_names.clear();
176 
177  for(const std::string& id : remove_ids) {
178  remove_names.insert(addon_titles_map[id]);
179  }
180 
181  const std::string confirm_message = _n(
182  "Are you sure you want to remove the following installed add-on?",
183  "Are you sure you want to remove the following installed add-ons?",
184  remove_ids.size()) + list_lead + utils::bullet_list(remove_names);
185 
186  res = gui2::show_message(
187  _("Confirm")
188  , confirm_message
190  } while (res != gui2::retval::OK);
191 
192  std::set<std::string> failed_names, skipped_names, succeeded_names;
193 
194  for(const std::string& id : remove_ids) {
195  const std::string& name = addon_titles_map[id];
196 
198  skipped_names.insert(name);
199  } else if(remove_local_addon(id)) {
200  succeeded_names.insert(name);
201  } else {
202  failed_names.insert(name);
203  }
204  }
205 
206  if(!skipped_names.empty()) {
207  const std::string dlg_msg = _n(
208  "The following add-on appears to have publishing or version control information stored locally, and will not be removed:",
209  "The following add-ons appear to have publishing or version control information stored locally, and will not be removed:",
210  skipped_names.size());
211 
213  dlg_msg + list_lead + utils::bullet_list(skipped_names));
214  }
215 
216  if(!failed_names.empty()) {
218  "The following add-on could not be deleted properly:",
219  "The following add-ons could not be deleted properly:",
220  failed_names.size()) + list_lead + utils::bullet_list(failed_names));
221  }
222 
223  if(!succeeded_names.empty()) {
224  const std::string dlg_title =
225  _n("Add-on Deleted", "Add-ons Deleted", succeeded_names.size());
226  const std::string dlg_msg = _n(
227  "The following add-on was successfully deleted:",
228  "The following add-ons were successfully deleted:",
229  succeeded_names.size());
230 
232  dlg_title,
233  dlg_msg + list_lead + utils::bullet_list(succeeded_names)
234  );
235 
236  return true;
237  }
238 
239  return false;
240 }
241 
242 } // end anonymous namespace
243 
245 {
246  static const int addon_download = 0;
247  // NOTE: the following two values are also known by WML, so don't change them.
248  static const int addon_uninstall = 2;
249 
250  std::string host_name = prefs::get().campaign_server();
251  const bool have_addons = !installed_addons().empty();
252 
253  gui2::dialogs::addon_connect addon_dlg(host_name, have_addons);
254  addon_dlg.show();
255  int res = addon_dlg.get_retval();
256 
257  if(res == gui2::retval::OK) {
258  res = addon_download;
259  }
260 
261  switch(res) {
262  case addon_download:
263  return addons_manager_ui(host_name);
264  case addon_uninstall:
265  return uninstall_local_addons();
266  default:
267  return false;
268  }
269 }
270 
271 bool ad_hoc_addon_fetch_session(const std::vector<std::string>& addon_ids)
272 {
273  std::string remote_address = prefs::get().campaign_server();
274 
275  // These exception handlers copied from addon_manager_ui fcn above.
276  try {
277 
278  addons_client client(remote_address);
279  client.connect();
280 
281  addons_list addons;
282 
283  if(!get_addons_list(client, addons)) {
284  gui2::show_error_message(_("An error occurred while downloading the add-ons list from the server."));
285  return false;
286  }
287 
288  bool return_value = true;
289  std::ostringstream os;
290  for(const std::string& addon_id : addon_ids) {
291  addons_list::const_iterator it = addons.find(addon_id);
292  if(it != addons.end()) {
293  const addon_info& addon = it->second;
294  const std::string addon_dir = filesystem::get_addons_dir()+"/"+addon_id;
295  const std::string info_cfg = addon_dir+"/_info.cfg";
296 
297  // no _info.cfg, so either there's a _server.pbl or there's no version information available at all, so this add-on can be skipped
298  if(filesystem::file_exists(addon_dir) && !filesystem::file_exists(info_cfg)) {
299  INFO_CFG << "No _info.cfg exists for '" << addon_id << "', skipping update.\n";
300  continue;
301  }
302 
303  // if _info.cfg exists, compare the local vs remote add-on versions to determine whether a download is needed
304  if(filesystem::file_exists(info_cfg)) {
306  version_info installed_addon_version(cache.get_config(info_cfg).child_or_empty("info")["version"]);
307 
308  // if the installed version is outdated, download the most recent version from the add-ons server
309  if(installed_addon_version >= addon.current_version) {
310  continue;
311  }
312  }
313 
314  // if the add-on exists locally and needs to be updated, or it doesn't exist and needs to be downloaded
315  addons_client::install_result res = client.install_addon_with_checks(addons, addon);
316  return_value = return_value && (res.outcome == addons_client::install_outcome::success);
317  } else {
318  if(!return_value) {
319  os << ", ";
320  }
321  os << addon_id;
322  return_value = false;
323  }
324  }
325 
326  if(!return_value) {
327  utils::string_map symbols;
328  symbols["addon_ids"] = os.str();
329  gui2::show_error_message(VGETTEXT("Could not find add-ons matching the ids $addon_ids on the add-on server.", symbols));
330  }
331 
332  return return_value;
333 
334  } catch(const config::error& e) {
335  ERR_CFG << "config::error thrown during transaction with add-on server; \""<< e.message << "\"";
336  gui2::show_error_message(_("Network communication error."));
337  } catch(const network_asio::error& e) {
338  ERR_NET << "network_asio::error thrown during transaction with add-on server; \""<< e.what() << "\"";
339  gui2::show_error_message(format_remote_disconnect_message(e));
340  } catch(const filesystem::io_exception& e) {
341  ERR_FS << "io_exception thrown while installing an addon; \"" << e.what() << "\"";
342  gui2::show_error_message(_("A problem occurred when trying to create the files necessary to install this add-on."));
343  } catch(const invalid_pbl_exception& e) {
344  ERR_CFG << "could not read .pbl file " << e.path << ": " << e.message;
345 
346  utils::string_map symbols;
347  symbols["path"] = e.path;
348  symbols["msg"] = e.message;
349 
351  VGETTEXT("A local file with add-on publishing information could not be read.\n\nFile: $path\nError message: $msg", symbols));
352  } catch(const wml_exception& e) {
353  e.show();
354  } catch(const addons_client::user_exit&) {
355  LOG_AC << "initial connection canceled by user";
356  } catch(const addons_client::invalid_server_address&) {
357  gui2::show_error_message(_("The add-ons server address specified is not valid."));
358  }
359 
360  return false;
361 }
bool remove_local_addon(const std::string &addon)
Removes the specified add-on, deleting its full directory structure.
Definition: manager.cpp:138
bool have_addon_in_vcs_tree(const std::string &addon_name)
Returns whether the specified add-on appears to be managed by a VCS or not.
Definition: manager.cpp:57
void get_addon_install_info(const std::string &addon_name, config &cfg)
Gets the installation info (_info.cfg) for an add-on.
Definition: manager.cpp:102
bool have_addon_pbl_info(const std::string &addon_name)
Returns whether a .pbl file is present for the specified add-on or not.
Definition: manager.cpp:66
std::vector< std::string > installed_addons()
Retrieves the names of all installed add-ons.
Definition: manager.cpp:186
bool have_addon_install_info(const std::string &addon_name)
Returns true if there is a local installation info (_info.cfg) file for the add-on.
Definition: manager.cpp:97
Add-ons (campaignd) client class.
Definition: client.hpp:41
install_result install_addon_with_checks(const addons_list &addons, const addon_info &addon)
Performs an add-on download and install cycle.
Definition: client.cpp:725
@ success
The add-on was correctly installed.
void connect()
Tries to establish a connection to the add-ons server.
Definition: client.cpp:69
bool request_addons_list(config &cfg, bool icons)
Request the add-ons list from the server.
Definition: client.cpp:253
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
const config & child_or_empty(std::string_view key) const
Returns the first child with the given key, or an empty config if there is none.
Definition: config.cpp:390
bool empty() const
Definition: config.cpp:823
Singleton class to manage game config file caching.
static config_cache & instance()
Get reference to the singleton object.
config get_config(const std::string &path, abstract_validator *validator=nullptr)
Gets a config object from given path.
bool get_need_wml_cache_refresh() const
Definition: manager.hpp:39
std::vector< std::string > selected_addons() const
@ yes_no_buttons
Shows a yes and no button.
Definition: message.hpp:81
bool show(const unsigned auto_close_time=0)
Shows the window.
int get_retval()
Definition: window.hpp:394
static prefs & get()
void set_campaign_server(const std::string &host)
std::string campaign_server()
Represents version numbers.
Networked add-ons (campaignd) client interface.
const config * cfg
Declarations for File-IO.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
static std::string _n(const char *str1, const char *str2, int n)
Definition: gettext.hpp:101
static std::string _(const char *str)
Definition: gettext.hpp:97
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:199
std::string make_addon_title(const std::string &id)
Replaces underscores to dress up file or dirnames as add-on titles.
Definition: info.cpp:299
void read_addons_list(const config &cfg, addons_list &dest)
Parse the specified add-ons list WML into an actual addons_list object.
Definition: info.cpp:277
std::map< std::string, addon_info > addons_list
Definition: info.hpp:27
Standard logging facilities (interface).
static lg::log_domain log_filesystem("filesystem")
#define ERR_CFG
Definition: manager_ui.cpp:41
#define INFO_CFG
Definition: manager_ui.cpp:42
#define LOG_AC
Definition: manager_ui.cpp:48
bool ad_hoc_addon_fetch_session(const std::vector< std::string > &addon_ids)
Conducts an ad-hoc add-ons server connection to download an add-on with a particular id and all it's ...
Definition: manager_ui.cpp:271
static lg::log_domain log_addons_client("addons-client")
static lg::log_domain log_network("network")
#define ERR_NET
Definition: manager_ui.cpp:44
bool manage_addons()
Shows the add-ons server connection dialog, for access to the various management front-ends.
Definition: manager_ui.cpp:244
#define ERR_FS
Definition: manager_ui.cpp:46
static lg::log_domain log_config("config")
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:344
std::string get_addons_dir()
void show_transient_message(const std::string &title, const std::string &message, const std::string &image, const bool message_use_markup, const bool title_use_markup)
Shows a transient message to the user.
void show_error_message(const std::string &msg, bool message_use_markup)
Shows an error message to the user.
Definition: message.cpp:201
void show_message(const std::string &title, const std::string &msg, const std::string &button_caption, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Shows a message to the user.
Definition: message.cpp:148
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
std::string bullet_list(const T &v, std::size_t indent=4, const std::string &bullet=font::unicode_bullet)
Generates a new string containing a bullet list.
std::map< std::string, t_string > string_map
version_info current_version
Definition: info.hpp:81
Contains the outcome of an add-on install operation.
Definition: client.hpp:134
install_outcome outcome
Overall outcome of the operation.
Definition: client.hpp:138
An exception object used when an IO error occurs.
Definition: filesystem.hpp:67
Exception thrown when the WML parser fails to read a .pbl file.
Definition: manager.hpp:45
Helper class, don't construct this directly.
Add a special kind of assert to validate whether the input from WML doesn't contain any problems that...
#define e