Freeciv21
Develop your civilization from humble roots to a global empire
download.cpp
Go to the documentation of this file.
1 /*__ ___ ***************************************
2 / \ / \ Copyright (c) 1996-2020 Freeciv21 and Freeciv
3 \_ \ / __/ contributors. This file is part of Freeciv21.
4  _\ \ / /__ Freeciv21 is free software: you can redistribute it
5  \___ \____/ __/ and/or modify it under the terms of the GNU General
6  \_ _/ Public License as published by the Free Software
7  | @ @ \_ Foundation, either version 3 of the License,
8  | or (at your option) any later version.
9  _/ /\ You should have received a copy of the GNU
10  /o) (o/\ \_ General Public License along with Freeciv21.
11  \_____/ / If not, see https://www.gnu.org/licenses/.
12  \____/ ********************************************************/
13 
14 #include <fc_config.h>
15 
16 #include <cerrno>
17 
18 // Qt
19 #include <QDir>
20 #include <QJsonArray>
21 #include <QJsonDocument>
22 #include <QJsonObject>
23 #include <QString>
24 #include <QUrl>
25 
26 // dependencies
27 #include "cvercmp.h"
28 
29 // utility
30 #include "capability.h"
31 #include "fcintl.h"
32 #include "netfile.h"
33 
34 // tools
35 #include "mpdb.h"
36 
37 #include "download.h"
38 
39 namespace /* anonymous */ {
44 class file_info {
45 public:
47  QString source() const { return m_source; }
48 
50  QFileInfo destination(const QString &prefix) const
51  {
52  return QFileInfo(prefix + m_destination);
53  }
54 
56  bool is_valid() const { return m_error.isEmpty(); }
57 
59  QString error() const { return m_error; }
60 
65  static file_info from_json(const QJsonValue &input);
66 
67 private:
69  file_info()
70  : m_source(), m_destination(),
71  m_error(QString::fromUtf8(_("Invalid file")))
72  {
73  }
74 
76  file_info(const QString &source, const QString &destination)
77  : m_source(source), m_destination(destination), m_error()
78  {
79  validate();
80  }
81 
83  file_info(const QString &source_destination)
84  : file_info(source_destination, source_destination)
85  {
86  }
87 
89  void set_error(const QString &error) { m_error = error; }
90 
92  void validate()
93  {
94  if (m_destination.isEmpty()) {
95  // Probably a mistake. Don't accept it.
96  set_error(QString::fromUtf8(_("Empty path")));
97  } else if (m_destination.contains(QStringLiteral(".."))) {
98  // Big no, might overwrite system files...
99  set_error(
100  QString::fromUtf8(_("Illegal path \"%1\"")).arg(m_destination));
101  }
102  }
103 
104  QString m_source;
105  QString m_destination;
106  QString m_error;
107 };
108 
109 file_info file_info::from_json(const QJsonValue &input)
110 {
111  if (input.isString()) {
112  // Option 1: source and destination of the same name
113  return file_info(input.toString());
114  } else if (input.isObject()) {
115  // Option 2: source and destination separately
116  // Convert to a QJsonObject
117  auto obj = input.toObject();
118 
119  if (!obj.contains("dest") || !obj["dest"].isString()) {
120  auto err = file_info();
121  // TRANS: Do not translate "dest" (stands for "destination")
122  err.set_error(QString::fromUtf8(_("Missing \"dest\" field")));
123  return err;
124  }
125  auto destination = obj["dest"].toString();
126 
127  if (obj.contains("url")) {
128  if (obj["url"].isString()) {
129  return file_info(obj["url"].toString(), destination);
130  } else {
131  auto err = file_info();
132  // TRANS: Do not translate "url"
133  err.set_error(QString::fromUtf8(_("\"url\" field is not a string")));
134  return err;
135  }
136  } else {
137  return file_info(destination);
138  }
139  } else {
140  // Invalid
141  return file_info();
142  }
143 }
144 
145 } // anonymous namespace
146 
150 const char *download_modpack(const QUrl &url, const struct fcmp_params *fcmp,
151  const dl_msg_callback &mcb,
152  const dl_pb_callback &pbcb, int recursion)
153 {
154  if (recursion > 5) {
155  return _("Recursive dependencies too deep");
156  }
157 
158  if (!url.isValid()) {
159  return _("No valid URL given");
160  }
161 
162  if (!url.fileName().endsWith(QStringLiteral(MODPACKDL_SUFFIX))) {
163  return _("This does not look like modpack URL");
164  }
165 
166  qInfo().noquote() << QString::fromUtf8(_("Installing modpack %1 from %2"))
167  .arg(url.fileName())
168  .arg(url.toString());
169 
170  if (fcmp->inst_prefix.isEmpty()) {
171  return _("Cannot install to given directory hierarchy");
172  }
173 
174  if (mcb != nullptr) {
175  // TRANS: %s is a filename with suffix '.modpack'
176  mcb(QString::fromUtf8(_("Downloading \"%1\" control file."))
177  .arg(url.fileName()));
178  }
179 
180  auto json = netfile_get_json_file(url, mcb);
181  if (!json.isObject()) {
182  return _("Cannot fetch and parse modpack list");
183  }
184 
185  auto info_value = json["info"];
186  if (!info_value.isObject()) {
187  // TRANS: Do not translate "info"
188  return _("\"info\" is not an object");
189  }
190  auto info = info_value.toObject();
191 
192  if (!info["options"].isString()) {
193  // TRANS: Do not translate "info.options"
194  return _("\"info.options\" is not a string");
195  }
196 
197  auto list_capstr = info["options"].toString();
198 
199  if (!has_capabilities(MODPACK_CAPSTR, qUtf8Printable(list_capstr))) {
200  qCritical() << "Incompatible control file:";
201  qCritical() << " control file options:" << list_capstr;
202  qCritical() << " supported options:" << MODPACK_CAPSTR;
203 
204  return _("Modpack control file is incompatible");
205  }
206 
207  if (!info["name"].isString()) {
208  // TRANS: Do not translate "info.name"
209  return _("\"info.name\" is not a string");
210  }
211  auto mpname = info["name"].toString();
212  if (mpname.isEmpty()) {
213  return _("Modpack name is empty");
214  }
215 
216  if (!info["version"].isString()) {
217  // TRANS: Do not translate "info.version"
218  return _("\"info.version\" is not a string");
219  }
220  auto mpver = info["version"].toString();
221 
222  if (!info["type"].isString()) {
223  // TRANS: Do not translate "info.type"
224  return _("\"info.type\" is not a string");
225  }
226  auto mptype = info["type"].toString();
227  auto type = modpack_type_by_name(qUtf8Printable(mptype), fc_strcasecmp);
228  if (!modpack_type_is_valid(type)) {
229  return _("Illegal modpack type");
230  }
231 
232  if (!info["base_url"].isString()) {
233  // TRANS: Do not translate "info.type"
234  return _("\"info.base_url\" is not a string");
235  }
236  auto base_url = QUrl(info["base_url"].toString());
237  base_url = url.resolved(base_url);
238 
239  // Make sure the url is treated as a directory by resolved(). For this we
240  // need to append a / if QUrl considers that there is a file name.
241  if (!base_url.fileName().isEmpty()) {
242  base_url.setPath(base_url.path(QUrl::FullyEncoded)
243  + QStringLiteral("/"));
244  }
245 
246  /*
247  * Fetch dependencies
248  */
249  auto deps = json["dependencies"];
250  if (!deps.isUndefined()) {
251  if (!deps.isArray()) {
252  // TRANS: Do not translate "dependencies"
253  return _("\"dependencies\" is not an array");
254  }
255 
256  for (const auto &depref : deps.toArray()) {
257  if (!depref.isObject()) {
258  // TRANS: Do not translate "dependencies"
259  return _("\"dependencies\" contains a non-object");
260  }
261 
262  // QJsonValueRef doesn't support operator[], convert to a QJsonObject
263  auto obj = depref.toObject();
264 
265  if (!obj.contains("url") || !obj["url"].isString()) {
266  // TRANS: Do not translate "url"
267  return _("Dependency has no \"url\" field or it is not a string");
268  }
269  auto dep_url = obj["url"].toString();
270 
271  if (!obj.contains("modpack") || !obj["modpack"].isString()) {
272  // TRANS: Do not translate "modpack"
273  return _(
274  "Dependency has no \"modpack\" field or it is not a string");
275  }
276  auto dep_name = obj["modpack"].toString();
277 
278  if (!obj.contains("type") || !obj["type"].isString()) {
279  // TRANS: Do not translate "modpack"
280  return _("Dependency has no \"type\" field or it is not a string");
281  }
282  auto dep_type_str = obj["type"].toString();
283 
284  auto dep_type =
285  modpack_type_by_name(qUtf8Printable(dep_type_str), fc_strcasecmp);
286  if (!modpack_type_is_valid(type)) {
287  qCritical() << "Illegal modpack type" << dep_type_str;
288  return _("Illegal modpack type");
289  }
290 
291  if (!obj.contains("version") || !obj["version"].isString()) {
292  // TRANS: Do not translate "version"
293  return _(
294  "Dependency has no \"version\" field or it is not a string");
295  }
296  auto dep_version = obj["version"].toString();
297 
298  // We have everything
299  auto inst_ver =
300  mpdb_installed_version(qUtf8Printable(dep_name), dep_type);
301  if (!inst_ver || !cvercmp_max(qUtf8Printable(dep_version), inst_ver)) {
302  qInfo() << "Dependency modpack" << QString(inst_ver) << dep_version
303  << "needed.";
304 
305  if (mcb != nullptr) {
306  mcb(_("Download dependency modpack"));
307  }
308 
309  auto dep_qurl = QUrl(dep_url);
310  if (dep_qurl.isRelative()) {
311  dep_qurl = url.resolved(dep_qurl);
312  }
313 
314  auto msg =
315  download_modpack(dep_qurl, fcmp, mcb, pbcb, recursion + 1);
316 
317  if (msg != nullptr) {
318  return msg;
319  }
320  }
321  delete[] inst_ver;
322  }
323  }
324 
325  /*
326  * Get the list of files
327  */
328  std::vector<file_info> required_files;
329 
330  auto files = json["files"];
331  if (!files.isArray()) {
332  // TRANS: Do not translate "files"
333  return _("\"files\" is not an array");
334  }
335 
336  std::size_t i = 0;
337  for (const auto &fref : files.toArray()) {
338  auto info = file_info::from_json(fref);
339  if (!info.is_valid()) {
340  // This doesn't look like a valid file
341  auto error = info.error();
342  qWarning().noquote()
343  << QString::fromUtf8(
344  _("Error parsing modpack control file: file %1:"))
345  .arg(i);
346  qWarning().noquote() << error;
347  if (mcb) {
348  mcb(error);
349  }
350  return _("Error parsing modpack control file");
351  }
352 
353  required_files.push_back(info);
354  i++;
355  }
356 
357  // Control file already downloaded
358  int downloaded = 1;
359  if (pbcb != nullptr) {
360  pbcb(downloaded, required_files.size() + 1);
361  }
362 
363  // Where to install?
364  auto local_dir =
366  + ((type == MPT_SCENARIO) ? QStringLiteral("/scenarios/")
367  : QStringLiteral("/" DATASUBDIR "/"));
368 
369  // Download and install
370  bool full_success = true;
371  for (auto info : required_files) {
372  auto destination = info.destination(local_dir);
373 
374  // Create the destination directory if needed
375  qDebug() << "Create directory:" << destination.absolutePath();
376  if (!destination.absoluteDir().mkpath(".")) {
377  return _("Cannot create required directories");
378  }
379 
380  if (mcb != nullptr) {
381  mcb(QString::fromUtf8(_("Downloading %1")).arg(info.source()));
382  }
383 
384  // Resolve the URL
385  auto source = base_url.resolved(info.source());
386  qDebug() << "Download" << source.toDisplayString() << "to"
387  << destination.absoluteFilePath();
388 
390  source, qUtf8Printable(destination.absoluteFilePath()), mcb)) {
391  if (mcb != nullptr) {
392  mcb(QString::fromUtf8(_("Failed to download %1"))
393  .arg(info.source()));
394  }
395  full_success = false;
396  }
397 
398  if (pbcb != nullptr) {
399  // Count download of control file also
400  downloaded++;
401  pbcb(downloaded, required_files.size() + 1);
402  }
403  }
404 
405  if (!full_success) {
406  return _("Some parts of the modpack failed to install.");
407  }
408 
409  mpdb_update_modpack(qUtf8Printable(mpname), type, qUtf8Printable(mpver));
410 
411  return nullptr;
412 }
413 
417 const char *download_modpack_list(const struct fcmp_params *fcmp,
418  const modpack_list_setup_cb &cb,
419  const dl_msg_callback &mcb)
420 {
421  auto json = netfile_get_json_file(fcmp->list_url, mcb);
422  if (!json.isObject()) {
423  return _("Cannot fetch and parse modpack list");
424  }
425 
426  auto info = json["info"];
427  if (!info.isObject()) {
428  // TRANS: Do not translate "info"
429  return _("\"info\" is not an object");
430  }
431 
432  if (!info["options"].isString()) {
433  // TRANS: Do not translate "info.options"
434  return _("\"info.options\" is not a string");
435  }
436 
437  auto list_capstr = info["options"].toString();
438 
439  if (!has_capabilities(MODLIST_CAPSTR, qUtf8Printable(list_capstr))) {
440  qCritical() << "Incompatible modpack list file:";
441  qCritical() << " list file options:" << list_capstr;
442  qCritical() << " supported options:" << MODLIST_CAPSTR;
443 
444  return _("Modpack list is incompatible");
445  }
446 
447  if (info["message"].isString()) {
448  mcb(info["message"].toString());
449  }
450 
451  auto modpacks = json["modpacks"];
452  if (!modpacks.isArray()) {
453  // TRANS: Do not translate "modpacks"
454  return _("\"modpacks\" is not an array");
455  }
456 
457  for (const auto &mpref : modpacks.toArray()) {
458  // QJsonValueRef doesn't support operator[], convert to a QJsonObject
459  if (!mpref.isObject()) {
460  // TRANS: Do not translate "modpacks"
461  return _("\"modpacks\" contains a non-object");
462  }
463  auto mp = mpref.toObject();
464 
465  // Modpack name (required)
466  if (!mp["name"].isString()) {
467  // TRANS: Do not translate "name"
468  return _("Modpack \"name\" is missing or is not a string");
469  }
470  auto name = mp["name"].toString();
471  if (name.isEmpty()) {
472  return _("Modpack name is empty");
473  }
474 
475  // Modpack version (required, can be empty)
476  if (!mp["version"].isString()) {
477  // TRANS: Do not translate "version"
478  return _("Modpack \"version\" is missing or is not a string");
479  }
480  auto version = mp["version"].toString();
481 
482  // Modpack license (required, can be empty)
483  if (!mp["license"].isString()) {
484  // TRANS: Do not translate "license"
485  return _("Modpack \"license\" is missing or is not a string");
486  }
487  auto license = mp["license"].toString();
488 
489  // Modpack type (required, validated)
490  if (!mp["type"].isString()) {
491  // TRANS: Do not translate "type"
492  return _("Modpack \"type\" is missing or is not a string");
493  }
494  auto type_str = mp["type"].toString();
495 
496  auto type =
497  modpack_type_by_name(qUtf8Printable(type_str), fc_strcasecmp);
498  if (!modpack_type_is_valid(type)) {
499  qCritical() << "Illegal modpack type" << type_str;
500  return _("Illegal modpack type");
501  }
502 
503  // Modpack subtype (optional, free text)
504  if (mp.contains("subtype") && !mp["subtype"].isString()) {
505  // TRANS: Do not translate "subtype"
506  return _("Modpack \"subtype\" is not a string");
507  }
508  auto subtype = mp["subtype"].toString(QStringLiteral("-"));
509 
510  // Modpack URL (optional, validated)
511  if (!mp["url"].isString()) {
512  // TRANS: Do not translate "url"
513  return _("Modpack \"url\" is missing or is not a string");
514  }
515  auto url = QUrl(mp["url"].toString());
516  if (!url.isValid()) {
517  qCritical() << "Invalid URL" << mp["url"].toString() << ":"
518  << url.errorString();
519  return _("Invalid URL");
520  }
521  auto resolved = url.isRelative() ? fcmp->list_url.resolved(url) : url;
522 
523  // Modpack notes (optional)
524  if (mp.contains("notes") && !mp["notes"].isString()) {
525  // TRANS: Do not translate "notes"
526  return _("Modpack \"notes\" is not a string");
527  }
528  auto notes = mp["notes"].toString(QStringLiteral(""));
529 
530  // Call the callback with the modpack info we just parsed
531  cb(name, resolved, version, license, type,
532  QString::fromUtf8(_(qUtf8Printable(subtype))), notes);
533  }
534 
535  return nullptr;
536 }
bool has_capabilities(const char *us, const char *them)
This routine returns true if all the mandatory capabilities in us appear in them.
Definition: capability.cpp:80
const char * download_modpack(const QUrl &url, const struct fcmp_params *fcmp, const dl_msg_callback &mcb, const dl_pb_callback &pbcb, int recursion)
Download modpack from a given URL.
Definition: download.cpp:150
const char * download_modpack_list(const struct fcmp_params *fcmp, const modpack_list_setup_cb &cb, const dl_msg_callback &mcb)
Download modpack list.
Definition: download.cpp:417
std::function< void(int downloaded, int max)> dl_pb_callback
Definition: download.h:34
#define MODLIST_CAPSTR
Definition: download.h:29
nf_errmsg dl_msg_callback
Definition: download.h:33
#define MODPACKDL_SUFFIX
Definition: download.h:26
#define MODPACK_CAPSTR
Definition: download.h:28
std::function< void(const QString &name, const QUrl &url, const QString &version, const QString &license, enum modpack_type type, const QString &subtype, const QString &notes)> modpack_list_setup_cb
Definition: download.h:43
#define _(String)
Definition: fcintl.h:50
const char * name
Definition: inputfile.cpp:118
struct fcmp_params fcmp
Definition: mpcli.cpp:35
const char * mpdb_installed_version(const char *name, enum modpack_type type)
Return version of modpack.
Definition: mpdb.cpp:251
bool mpdb_update_modpack(const char *name, enum modpack_type type, const char *version)
Update modpack information in database.
Definition: mpdb.cpp:209
bool netfile_download_file(const QUrl &url, const char *filename, const nf_errmsg &cb)
Fetch file from given URL and save as given filename.
Definition: netfile.cpp:139
QJsonDocument netfile_get_json_file(const QUrl &url, const nf_errmsg &cb)
Fetch a JSON file from the net.
Definition: netfile.cpp:98
static int recursion[AIT_LAST]
Definition: srv_log.cpp:35
QUrl list_url
Definition: modinst.h:20
QString inst_prefix
Definition: modinst.h:21
int fc_strcasecmp(const char *str0, const char *str1)
Compare strings like strcmp(), but ignoring case.
Definition: support.cpp:89